Issues (3627)

bundles/CoreBundle/Command/ModeratedCommand.php (2 issues)

1
<?php
2
3
/*
4
 * @copyright   2015 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\CoreBundle\Command;
13
14
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Filesystem\Exception\IOException;
19
use Symfony\Component\Filesystem\LockHandler;
20
21
abstract class ModeratedCommand extends ContainerAwareCommand
22
{
23
    const MODE_LOCK   = 'lock';
24
    const MODE_PID    = 'pid';
25
    const MODE_FLOCK  = 'flock';
26
27
    protected $checkFile;
28
    protected $moderationKey;
29
    protected $moderationTable = [];
30
    protected $moderationMode  = self::MODE_LOCK;
31
    protected $runDirectory;
32
    protected $lockExpiration = false;
33
    protected $lockHandler;
34
    protected $lockFile;
35
    private $bypassLocking;
36
37
    private $flockHandle;
38
39
    /* @var OutputInterface $output */
40
    protected $output;
41
42
    /**
43
     * Set moderation options.
44
     */
45
    protected function configure()
46
    {
47
        $this
48
            ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.')
49
            ->addOption('--bypass-locking', null, InputOption::VALUE_NONE, 'Bypass locking.')
50
            ->addOption(
51
                '--timeout',
52
                '-t',
53
                InputOption::VALUE_REQUIRED,
54
                'If getmypid() is disabled on this system, lock files will be used. This option will assume the process is dead after the specified number of seconds and will execute anyway. This is disabled by default.',
55
                false
56
            )
57
            ->addOption(
58
                '--lock_mode',
59
                '-x',
60
                InputOption::VALUE_REQUIRED,
61
                'Allowed value are "pid" , "file_lock" or "flock". By default, lock will try with pid, if not available will use file system',
62
                'pid'
63
            );
64
    }
65
66
    /**
67
     * @return bool
68
     */
69
    protected function checkRunStatus(InputInterface $input, OutputInterface $output, $moderationKey = '')
70
    {
71
        $this->output         = $output;
72
        $this->lockExpiration = $input->getOption('timeout');
73
        $this->bypassLocking  = $input->getOption('bypass-locking');
74
        $lockMode             = $input->getOption('lock_mode');
75
76
        if (!in_array($lockMode, ['pid', 'file_lock', 'flock'])) {
77
            $output->writeln('<error>Unknown locking method specified.</error>');
78
79
            return false;
80
        }
81
82
        // If bypass locking, then don't bother locking
83
        if ($this->bypassLocking) {
84
            return true;
85
        }
86
87
        // Allow multiple runs of the same command if executing different IDs, etc
88
        $this->moderationKey = $this->getName().$moderationKey;
89
90
        // Setup the run directory for lock/pid files
91
        $this->runDirectory = $this->getContainer()->getParameter('kernel.cache_dir').'/../run';
92
        if (!file_exists($this->runDirectory)) {
93
            if (!mkdir($this->runDirectory)) {
94
                $output->writeln('<error>'.$this->runDirectory.' could not be created.</error>');
95
96
                return false;
97
            }
98
        }
99
100
        $this->lockFile = sprintf(
101
            '%s/sf.%s.%s.lock',
102
            $this->runDirectory,
103
            preg_replace('/[^a-z0-9\._-]+/i', '-', $this->moderationKey),
104
            hash('sha256', $this->moderationKey)
105
        );
106
107
        // Check if the command is currently running
108
        if (!$this->checkStatus($input->getOption('force'), $lockMode)) {
109
            $output->writeln('<error>Script in progress. Can force execution by using --force.</error>');
110
111
            return false;
112
        }
113
114
        return true;
115
    }
116
117
    /**
118
     * Complete this run.
119
     */
120
    protected function completeRun()
121
    {
122
        if ($this->bypassLocking) {
123
            return;
124
        }
125
126
        if (self::MODE_LOCK == $this->moderationMode) {
127
            $this->lockHandler->release();
128
        }
129
        if (self::MODE_FLOCK == $this->moderationMode) {
130
            fclose($this->flockHandle);
131
        }
132
133
        // Attempt to keep things tidy
134
        @unlink($this->lockFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

134
        /** @scrutinizer ignore-unhandled */ @unlink($this->lockFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
135
    }
136
137
    /**
138
     * Determine the moderation mode avaiable to this system. Default is to use a lock file.
139
     *
140
     * @param bool   $force
141
     * @param string $lockMode
142
     *
143
     * @return bool
144
     */
145
    private function checkStatus($force = false, $lockMode = null)
146
    {
147
        // getmypid may be disabled and posix_getpgid is not available on Windows machines
148
        if ((is_null($lockMode) || 'pid' === $lockMode) && function_exists('getmypid') && function_exists('posix_getpgid')) {
149
            $disabled = explode(',', ini_get('disable_functions'));
150
            if (!in_array('getmypid', $disabled) && !in_array('posix_getpgid', $disabled)) {
151
                $this->moderationMode = self::MODE_PID;
152
153
                // Check if the PID is still running
154
                $fp = fopen($this->lockFile, 'c+');
155
                if (!flock($fp, LOCK_EX)) {
156
                    $this->output->writeln("<error>Failed to lock {$this->lockFile}.</error>");
157
158
                    return false;
159
                }
160
161
                $pid = fgets($fp, 8192);
162
                if (!$force && $pid && posix_getpgid($pid)) {
163
                    $this->output->writeln('<info>Script with pid '.$pid.' in progress.</info>');
164
165
                    flock($fp, LOCK_UN);
166
                    fclose($fp);
167
168
                    return false;
169
                }
170
171
                // Write current PID to lock file
172
                ftruncate($fp, 0);
173
                rewind($fp);
174
175
                fputs($fp, getmypid());
176
                fflush($fp);
177
178
                flock($fp, LOCK_UN);
179
                fclose($fp);
180
181
                return true;
182
            }
183
        } elseif (self::MODE_FLOCK === $lockMode && !$force) {
184
            $this->moderationMode = self::MODE_FLOCK;
185
            $error                = null;
186
            // Silence error reporting
187
            set_error_handler(function ($errno, $msg) use (&$error) {
188
                $error = $msg;
189
            });
190
191
            if (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) {
192
                if ($this->flockHandle = fopen($this->lockFile, 'x')) {
193
                    chmod($this->lockFile, 0666);
194
                } elseif (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) {
195
                    usleep(100);
196
                    $this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r');
197
                }
198
            }
199
200
            restore_error_handler();
201
202
            if (!$this->flockHandle) {
203
                throw new IOException($error, 0, null, $this->lockFile);
204
            }
205
            if (!flock($this->flockHandle, LOCK_EX | LOCK_NB)) {
206
                fclose($this->flockHandle);
207
                $this->flockHandle = null;
208
209
                return false;
210
            }
211
212
            return true;
213
        }
214
215
        // in anycase, fallback on file system
216
        // Accessing PID commands is not available so use a simple lock file mechanism
217
        $lockHandler = $this->lockHandler = new LockHandler($this->moderationKey, $this->runDirectory);
218
219
        if (!$force && !$lockHandler->lock()) {
220
            // Check timestamp if $force is not requested
221
            if ($this->lockExpiration) {
222
                $fileAge = time() - filemtime($this->lockFile);
223
224
                if ($fileAge <= $this->lockExpiration) {
225
                    $this->output->writeln('<info>Lock expires in '.($this->lockExpiration - $fileAge).' seconds.</info>');
226
227
                    return false;
228
                }
229
            } else {
230
                // Lock is still in effect
231
                return false;
232
            }
233
        } elseif (!$force) {
234
            // Attempt to update the modified time just in case there was no lock but the file still exists
235
            @touch($this->lockFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

235
            /** @scrutinizer ignore-unhandled */ @touch($this->lockFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
236
        }
237
238
        return true;
239
    }
240
}
241