Passed
Push — master ( 032384...6ce998 )
by Fabio
05:25
created

TCaptureForkLog::receiveLogsFromChildren()   D

Complexity

Conditions 26
Paths 8

Size

Total Lines 70
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 52
nc 8
nop 2
dl 0
loc 70
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * TCaptureForkLog class file.
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Util\Behaviors;
11
12
use Prado\Prado;
0 ignored issues
show
Bug introduced by
The type Prado\Prado was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Prado\Util\{TBehavior, IBaseBehavior};
14
use Prado\Util\TCallChain;
15
use Prado\Util\TLogger;
16
use Prado\Util\Helpers\TProcessHelper;
17
18
/**
19
 * TCaptureForkLog class.
20
 *
21
 * This captures the log of a child fork and sends it back to the parent thread.
22
 *
23
 * When {@see \Prado\Util\TProcessHelper::fork()} is called, `fxPrepareForFork`
24
 * is raised before the fore and `fxRestoreAfterFork` after the fork.
25
 * On `fxPrepareForFork`, this class creates a socket pair.  On `fxRestoreAfterFork`,
26
 * The parent stores all child connections, and the child sends all log data to
27
 * the parent.  The parent receives logs when processing logs.
28
 *
29
 * Before sending logs, a child will receive any logs for its own child processes.
30
 * When sending the final logs from the child, the socket is closed and the parent
31
 * is flagged to close the connection.
32
 *
33
 * When not the final processing of the logs, only the pending logs will be sent
34
 * and received before returning.  When Final, the parent will wait until all children
35
 * processes have returned their logs.
36
 *
37
 * Due to this class adding logs during the flushing of the logs
38
 *
39
 * Attach this behavior to the TApplication class or object.
40
 * ```xml
41
 *		<behavior name="appSignals" AttachToClass="Prado\TApplication" class="Prado\Util\Behaviors\TApplicationSignals" PriorHandlerPriority="5" />
42
 * ```
43
 *
44
 * @author Brad Anderson <[email protected]>
45
 * @since 4.2.3
46
 */
47
class TCaptureForkLog extends \Prado\Util\TBehavior
48
{
49
	public const BEHAVIOR_NAME = 'captureforklog';
50
51
	/** @var bool Is the master receiver (of child logs) installed. */
52
	private bool $_receiverInstalled = false;
53
54
	/** @var ?array The parent connections to each child fork receiving logs from. */
55
	protected ?array $_parentConnections = [];
56
57
	/** @var mixed The child connection to the parent */
58
	protected mixed $_childConnection = null;
59
60
	/**
61
	 * Installs {@see self::generateConnection()} on fxPrepareForFork and
62
	 * {@see self::configureForChildLogs()} on fxRestoreAfterFork.
63
	 * @return array Event callbacks for the behavior.
64
	 */
65
	public function events()
66
	{
67
		return [TProcessHelper::FX_PREPARE_FOR_FORK => 'generateConnection',
68
				TProcessHelper::FX_RESTORE_AFTER_FORK => 'configureForChildLogs'];
69
	}
70
71
	/**
72
	 *
73
	 * @return ?float The priority of the behavior, default -10 and not
74
	 *   the normal "null".
75
	 */
76
	public function getPriority(): ?float
77
	{
78
		if (($priority = parent::getPriority()) === null) {
79
			$priority = -10;
80
		}
81
		return $priority;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $priority returns the type double|integer which is incompatible with the return type mandated by Prado\Collections\IPriorityItem::getPriority() of Prado\Collections\numeric.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
82
	}
83
84
	/**
85
	 * This is the singleton behavior.  Only one instance of itself can be a behavior
86
	 * of the owner.
87
	 * @param string $name The name of teh behavior being added to the owner.
88
	 * @param IBaseBehavior $behavior The behavior being added to the owner.
89
	 * @param TCallChain $chain The chain of event handlers.
90
	 */
91
	public function dyAttachBehavior($name, $behavior, TCallChain $chain)
92
	{
93
		$owner = $this->getOwner();
94
		if (count($owner->getBehaviors(self::class)) > 1) {
95
			$owner->detachBehavior($name);
96
		}
97
		return $chain->dyAttachBehavior($name, $behavior);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $chain->dyAttachBehavior($name, $behavior) targeting Prado\TComponent::dyAttachBehavior() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
98
	}
99
100
	/**
101
	 * The behavior callback for fxPrepareForFork that creates a socket pair connection
102
	 * between the parent process and child process before forking.
103
	 * @param mixed $sender The TApplication doing the fork.
104
	 * @param mixed $param The parameter of fxPrepareForFork.
105
	 * @return ?array Any data to be passed back to the restore function.
106
	 *   eg. `return ['key', 'data'];` will be passed in the restore function as
107
	 *   `['key' => 'data', 'pid' => ###, ...]`.
108
	 */
109
	public function generateConnection(mixed $sender, mixed $param)
0 ignored issues
show
Unused Code introduced by
The parameter $sender is not used and could be removed. ( Ignorable by Annotation )

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

109
	public function generateConnection(/** @scrutinizer ignore-unused */ mixed $sender, mixed $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

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

109
	public function generateConnection(mixed $sender, /** @scrutinizer ignore-unused */ mixed $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
110
	{
111
		$domain = TProcessHelper::isSystemWindows() ? AF_INET : AF_UNIX;
112
		if (!socket_create_pair($domain, SOCK_STREAM, 0, $this->_childConnection)) {
113
			$this->_childConnection = null;
114
			return;
115
		}
116
		$this->_childConnection[0] = socket_export_stream($this->_childConnection[0]);
117
		$this->_childConnection[1] = socket_export_stream($this->_childConnection[1]);
118
		return null;
119
	}
120
121
	/**
122
	 * The behavior call back for fxRestoreAfterFork that cleans the log and resets
123
	 * the logger to send the log to the parent process.  The Parent process stores
124
	 * the child stream and pid and installs the onEndRequest handler to receive the
125
	 * logs from the children forks.
126
	 * @param mixed $sender
127
	 * @param array $data the
128
	 */
129
	public function configureForChildLogs(mixed $sender, mixed $data)
130
	{
131
		if (!$this->_childConnection) {
132
			return;
133
		}
134
		$pid = $data['pid'];
135
		if ($pid === -1) { //fail
136
			if ($this->_childConnection) {
137
				stream_socket_shutdown($this->_childConnection[0], STREAM_SHUT_RDWR);
138
				stream_socket_shutdown($this->_childConnection[1], STREAM_SHUT_RDWR);
139
				$this->_childConnection = null;
140
			}
141
		} elseif ($pid === 0) { // Child Process
142
			$this->_parentConnections = [];
143
			$this->_childConnection = $this->_childConnection[1];
144
			$logger = Prado::getLogger();
145
			$logger->deleteLogs();
146
			$logs = $logger->deleteProfileLogs();
147
			$pid = getmypid();
148
			foreach(array_keys($logs) as $key) { // Reset PROFILE_BEGIN to pid.
149
				$logs[$key][TLogger::LOG_LEVEL] &= ~TLogger::LOGGED;
150
				$logs[$key][TLogger::LOG_PID] = $pid;
151
			}
152
			$logger->mergeLogs($logs); // Profiler logs with child pid
153
			$logger->getEventHandlers('onFlushLogs')->clear();
154
			$logger->attachEventHandler('onFlushLogs', [$this, 'sendLogsToParent'], $this->getPriority());
155
			$this->_receiverInstalled = true;
156
		} else { // Parent Process
157
			$this->_parentConnections[$pid] = $this->_childConnection[0];
158
			$this->_childConnection = null;
159
			if (!$this->_receiverInstalled) {
160
				$logger = Prado::getLogger();
161
				$logger->attachEventHandler('onCollectLogs', [$this, 'receiveLogsFromChildren'], $this->getPriority());
162
				$this->_receiverInstalled = true;
163
			}
164
		}
165
	}
166
167
	/**
168
	 * Receives logs from children forked processes and merges the logs with the current
169
	 * application TLogger.
170
	 * @param ?int $pid The process ID to receive the logs from, default null for all.
171
	 * @param bool $wait Wait for results until complete, default true.  When false,
172
	 *   this will process the pending logs and not wait for further logs.
173
	 */
174
	public function receiveLogsFromChildren(?int $pid = null, bool $wait = true)
175
	{
176
		if (!$this->_parentConnections) {
177
			return;
178
		}
179
		if($pid && !isset($this->_parentConnections[$pid])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pid of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
180
			return;
181
		}
182
183
		$completeLogs = [];
184
		$write = $except = [];
185
		$connections = $this->_parentConnections;
186
		$childLogs = [];
187
		do {
188
			$read = $connections;
189
			if (stream_select($read, $write, $except, ($wait || count($childLogs)) ? 1 : 0, 0)) {
190
				foreach($read as $pid => $socket) {
191
					$data = fread($socket, 8192);
192
					do {
193
						$iterate = false;
194
						if($data !== false) {
195
							if (array_key_exists($pid, $childLogs)) {
196
								$childLogs[$pid][0] .= $data;
197
							} else {
198
								$length = substr($data, 0, 4);
199
								if (strlen($length) >= 4) {
200
									$length = unpack('N', $length)[1];
201
									$final = ($length & 0x80000000) != 0;
202
									$length &= 0x7FFFFFFF;
203
									if ($length) {
204
										$data = substr($data, 4);
205
										$childLogs[$pid] = [$data, abs($length), $final];
206
									} else {
207
										$childLogs[$pid] = ['', 0, $final];
208
									}
209
								} else {
210
									$childLogs[$pid] = ['', 0, true];
211
								}
212
							}
213
						} else {
214
							$childLogs[$pid] = ['', 0, true];
215
						}
216
						if (isset($childLogs[$pid]) && strlen($childLogs[$pid][0]) >= $childLogs[$pid][1]) {
217
							if ($childLogs[$pid][2]) {
218
								stream_socket_shutdown($socket, STREAM_SHUT_RDWR);
219
								unset($this->_parentConnections[$pid]);
220
								unset($connections[$pid]);
221
							}
222
							if ($childLogs[$pid][1]) {
223
								$completeLogs[$pid][] = substr($childLogs[$pid][0], 0, $childLogs[$pid][1]);
224
							}
225
							$data = substr($childLogs[$pid][0], $childLogs[$pid][1]);
226
							unset($childLogs[$pid]);
227
							if (!strlen($data)) {
228
								$data = false;
229
							} else {
230
								$iterate = true;
231
							}
232
						}
233
234
					} while ($iterate);
235
				}
236
			}
237
		} while(count($childLogs) || $wait && ($pid && isset($connections[$pid]) || $pid === null && $connections));
238
239
		if (!$completeLogs) {
240
			return;
241
		}
242
		foreach(array_merge(...$completeLogs) as $pid => $logs) {
243
			Prado::getLogger()->mergeLogs(unserialize($logs));
244
		}
245
	}
246
247
	/**
248
	 * First, Receives any logs from the children.  If this instance is a child fork
249
	 * then send the log to the parent process.  If the call is final then the connection
250
	 * to the parent is shutdown.
251
	 * @param mixed $logger The TLogger that raised the onFlushLogs event.
252
	 * @param mixed $final Is this the last and final call.
253
	 */
254
	public function sendLogsToParent($logger, $final)
255
	{
256
		if (!$this->_childConnection) {
257
			return;
258
		}
259
260
		if(!($logger instanceof TLogger)) {
261
			$logger = Prado::getLogger();
262
		}
263
264
		$logs = $logger->getLogs();
265
266
		{ // clear logs already logged.
267
			$reset = false;
268
			foreach ($logs as $key => $log) {
269
				if ($log[TLogger::LOG_LEVEL] & TLogger::LOGGED) {
270
					unset($logs[$key]);
271
					$reset = true;
272
				}
273
			}
274
			if ($reset) {
275
				$logs = array_values($logs);
276
			}
277
		}
278
279
		$data = serialize($logs);
280
281
		if (!$logs) {
282
			$data = '';
283
		}
284
285
		$data = pack('N', ($final ? 0x80000000 : 0) | ($length = strlen($data))) . $data;
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
286
		$count = null;
287
		$read = $except = [];
288
289
		do {
290
			$write = [$this->_childConnection];
291
			if (stream_select($read, $write, $except, 1, 0)) {
292
				$count = fwrite($this->_childConnection, $data);
293
				if ($count > 0) {
294
					$data = substr($data, $count);
295
				}
296
			}
297
		} while($count !== false && strlen($data) > 0);
298
299
		if ($final) {
300
			stream_socket_shutdown($this->_childConnection, STREAM_SHUT_RDWR);
301
			$this->_childConnection = null;
302
		}
303
	}
304
}
305