Process::compileCommand()   B
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 29
c 0
b 0
f 0
ccs 18
cts 18
cp 1
rs 8.439
cc 5
eloc 16
nc 16
nop 0
crap 5
1
<?php
2
3
namespace Basebuilder\Scheduling\Event;
4
5
use Symfony\Component\Process\Process as OsProcess;
6
use Symfony\Component\Process\ProcessUtils;
7
use Webmozart\Assert\Assert;
8
9
class Process extends BaseEvent
10
{
11
    /**
12
     * The command to run.
13
     * @var string
14
     */
15
    protected $command;
16
17
    /**
18
     * The working directory.
19
     * @var string
20
     */
21
    protected $cwd;
22
23
    /**
24
     * The user the command should run as.
25
     * @var string
26
     */
27
    protected $user;
28
29
    /**
30
     * Indicates if the command should not overlap itself.
31
     * @var bool
32
     */
33
    protected $mutuallyExclusive = false;
34
35
    /**
36
     * Indicates if the command should run in background.
37
     * @var bool
38
     */
39
    protected $runInBackground = true;
40
41
    /**
42
     * The location that output should be sent to.
43
     * @var string
44
     */
45
    protected $output = '/dev/null';
46
47
    /**
48
     * The location of error output
49
     * @var string
50
     */
51
    protected $errorOutput = '/dev/null';
52
53
    /**
54
     * Indicates whether output should be appended or added (> vs >>)
55
     * @var bool
56
     */
57
    protected $shouldAppendOutput = true;
58
59 5
    public function __construct(/* string */ $command)
60
    {
61 5
        Assert::stringNotEmpty($command);
62
63 5
        $this->command = $command;
64 5
    }
65
66
    /**
67
     * @return string
68
     */
69
    public function getCommand()
70
    {
71
        return $this->command;
72
    }
73
74
    /**
75
     * @return string
76
     */
77
    public function __toString()
78
    {
79
        return $this->getCommand();
80
    }
81
82
    /**
83
     * Build the command string.
84
     *
85
     * @return string
86
     */
87 5
    public function compileCommand()
88
    {
89 5
        $redirect    = $this->shouldAppendOutput ? '>>' : '>';
90 5
        $output      = ProcessUtils::escapeArgument($this->output);
91 5
        $errorOutput = ProcessUtils::escapeArgument($this->errorOutput);
92
93
        // e.g. 1>> /dev/null 2>> /dev/null
94 5
        $outputRedirect = ' 1' . $redirect . ' ' . $output . ' 2' . $redirect . ' ' . $errorOutput;
95
96 5
        $parts = [];
97
98 5
        if ($this->cwd) {
99 1
            $parts[] =  'cd ' . $this->cwd . ';';
100 1
        }
101
102 5
        if ($this->user) {
103 1
            $parts[] = 'sudo -u ' . $this->user . ' --';
104 1
        }
105
106 5
        $wrapped = $this->mutuallyExclusive
107 5
            ? '(touch ' . $this->getMutexPath() . '; ' . $this->command . '; rm ' . $this->getMutexPath() . ')' . $outputRedirect
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
108 5
            : $this->command . $outputRedirect;
109
110 5
        $parts[] = "sh -c '{$wrapped}'";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $wrapped instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
111
112 5
        $command = implode(' ', $parts);
113
114 5
        return $command;
115
    }
116
117
    /**
118
     * Run the given event.
119
     *
120
     * @return OsProcess
121
     */
122
    public function run()
123
    {
124
        foreach ($this->beforeCallbacks as $callback) {
125
            call_user_func($callback);
126
        }
127
128
        if (!$this->runInBackground) {
129
            $process = $this->runCommandInForeground();
130
        } else {
131
            $process = $this->runCommandInBackground();
132
        }
133
134
        foreach ($this->afterCallbacks as $callback) {
135
            call_user_func($callback);
136
        }
137
138
        return $process;
139
    }
140
141
    /**
142
     * Run the command in the foreground.
143
     *
144
     * @return OsProcess
145
     */
146
    protected function runCommandInForeground()
147
    {
148
        $process = new OsProcess($this->compileCommand(), $this->cwd, null, null, null);
149
        $process->run();
150
151
        return $process;
152
    }
153
154
    /**
155
     * Run the command in the background.
156
     *
157
     * @return OsProcess
158
     */
159
    protected function runCommandInBackground()
160
    {
161
        $process = new OsProcess($this->compileCommand(), $this->cwd, null, null, null);
162
        $process->start();
163
164
        return $process;
165
    }
166
167
    /**
168
     * Get the mutex path for managing concurrency
169
     *
170
     * @return string
171
     */
172
    protected function getMutexPath()
173
    {
174
        return rtrim(sys_get_temp_dir(), '/') . '/scheduled-event-' . md5($this->cwd . $this->command);
175
    }
176
177
    /**
178
     * Do not allow the event to overlap each other.
179
     *
180
     * @return $this
181
     */
182
    public function preventOverlapping()
183
    {
184
        $this->mutuallyExclusive = true;
185
186
        // Skip the event if it's locked (processing)
187
        $this->skip(function() {
188
            return $this->isLocked();
189
        });
190
191
        return $this;
192
    }
193
194
    /**
195
     * Tells you whether this event has been denied from mutual exclusiveness
196
     *
197
     * @return bool
198
     */
199
    protected function isLocked()
200
    {
201
        return file_exists($this->getMutexPath());
202
    }
203
204
    /**
205
     * State that the command should run in the foreground
206
     *
207
     * @return $this
208
     */
209
    public function runInForeground()
210
    {
211
        $this->runInBackground = false;
212
213
        return $this;
214
    }
215
216
    /**
217
     * State that the command should run in the background.
218
     *
219
     * @return $this
220
     */
221
    public function runInBackground()
222
    {
223
        $this->runInBackground = true;
224
225
        return $this;
226
    }
227
228
    /**
229
     * Set which user the command should run as.
230
     *
231
     * @param  string?  $user
0 ignored issues
show
Documentation introduced by
The doc-type string? could not be parsed: Unknown type name "string?" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
232
     * @return $this
233
     */
234 1
    public function asUser(/* string? */ $user)
235
    {
236 1
        Assert::nullOrString($user);
237
238 1
        $this->user = $user;
239
240 1
        return $this;
241
    }
242
243
    /**
244
     * Change the current working directory.
245
     *
246
     * @param  string $directory
247
     * @return $this
248
     */
249 1
    public function in(/* string */ $directory)
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 3 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
250
    {
251 1
        Assert::stringNotEmpty($directory);
252
253 1
        $this->cwd = $directory;
254
255 1
        return $this;
256
    }
257
258
    /**
259
     * Whether we append or redirect output
260
     *
261
     * @param bool $switch
262
     * @return $this
263
     */
264 2
    public function appendOutput(/* boolean */ $switch = true)
265
    {
266 2
        Assert::boolean($switch);
267
268 2
        $this->shouldAppendOutput = $switch;
269
270 2
        return $this;
271
    }
272
273
    /**
274
     * Set the file or location where to send file descriptor 1 to
275
     *
276
     * @param string $output
277
     * @return $this
278
     */
279
    public function outputTo(/* string */ $output)
280
    {
281
        Assert::stringNotEmpty($output);
282
283
        $this->output = $output;
284
285
        return $this;
286
    }
287
288
    /**
289
     * Set the file or location where to send file descriptor 2 to
290
     *
291
     * @param string $output
292
     * @return $this
293
     */
294
    public function errorOutputTo(/* string */ $output)
295
    {
296
        Assert::stringNotEmpty($output);
297
298
        $this->errorOutput = $output;
299
300
        return $this;
301
    }
302
}
303