Passed
Pull Request — master (#225)
by Alexander
04:20 queued 01:45
created

Connection::read()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 60
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 9
eloc 41
c 2
b 1
f 0
nc 9
nop 0
dl 0
loc 60
rs 7.7084

How to fix   Long Method   

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
/** @noinspection PhpComposerExtensionStubsInspection */
4
5
declare(strict_types=1);
6
7
namespace Yiisoft\Yii\Debug\DebugServer;
8
9
use Generator;
10
use RuntimeException;
11
use Socket;
12
use Throwable;
13
14
/**
15
 * List of socket errors: {@see https://www.ibm.com/docs/en/zos/2.4.0?topic=calls-sockets-return-codes-errnos}
16
 */
17
final class Connection
18
{
19
    public const DEFAULT_TIMEOUT = 10 * 1000; // 10 milliseconds
20
    public const DEFAULT_BUFFER_SIZE = 1 * 1024; // 1 kilobyte
21
22
    public const TYPE_RESULT = 0x001B;
23
    public const TYPE_ERROR = 0x002B;
24
    public const TYPE_RELEASE = 0x003B;
25
26
    public const MESSAGE_TYPE_VAR_DUMPER = 0x001B;
27
    public const MESSAGE_TYPE_LOGGER = 0x002B;
28
29
    private string $uri;
30
31
    public function __construct(
32
        private Socket $socket,
33
    ) {
34
    }
35
36
    public static function create(): self
37
    {
38
        $socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
39
40
        $socket_last_error = socket_last_error($socket);
41
42
        if ($socket_last_error) {
43
            throw new RuntimeException(
44
                sprintf(
45
                    '"socket_last_error" returned %d: "%s".',
46
                    $socket_last_error,
47
                    socket_strerror($socket_last_error),
48
                ),
49
            );
50
        }
51
52
        return new self(
53
            $socket,
0 ignored issues
show
Bug introduced by
It seems like $socket can also be of type resource; however, parameter $socket of Yiisoft\Yii\Debug\DebugS...nnection::__construct() does only seem to accept Socket, maybe add an additional type check? ( Ignorable by Annotation )

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

53
            /** @scrutinizer ignore-type */ $socket,
Loading history...
54
        );
55
    }
56
57
    public function bind(): void
58
    {
59
        $n = random_int(0, PHP_INT_MAX);
60
        $file = sprintf(sys_get_temp_dir() . '/yii-dev-server-%d.sock', $n);
61
        $this->uri = $file;
62
        if (!socket_bind($this->socket, $file)) {
63
            $socket_last_error = socket_last_error($this->socket);
64
65
            throw new RuntimeException(
66
                sprintf(
67
                    'An error occurred while reading the socket. "socket_last_error" returned %d: "%s".',
68
                    $socket_last_error,
69
                    socket_strerror($socket_last_error),
70
                ),
71
            );
72
        }
73
    }
74
75
    /**
76
     * @return Generator<int, array{0: self::TYPE_ERROR|self::TYPE_RELEASE|self::TYPE_RESULT, 1: string, 2: int|string, 3?: int}>
77
     */
78
    public function read(): Generator
79
    {
80
        $sndbuf = socket_get_option($this->socket, SOL_SOCKET, SO_SNDBUF);
0 ignored issues
show
Unused Code introduced by
The assignment to $sndbuf is dead and can be removed.
Loading history...
81
        $rcvbuf = socket_get_option($this->socket, SOL_SOCKET, SO_RCVBUF);
0 ignored issues
show
Unused Code introduced by
The assignment to $rcvbuf is dead and can be removed.
Loading history...
82
83
        socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 2, 'usec' => 0]);
84
        socket_set_option($this->socket, SOL_SOCKET, SO_RCVBUF, 1024 * 10);
85
        socket_set_option($this->socket, SOL_SOCKET, SO_SNDBUF, 1024 * 10);
86
87
        $newFrameAwaitRepeat = 0;
88
        $maxFrameAwaitRepeats = 10;
89
        $maxRepeats = 10;
90
91
        while (true) {
92
            if (!socket_recv($this->socket, $header, 8, MSG_WAITALL)) {
93
                $socket_last_error = socket_last_error($this->socket);
94
                $newFrameAwaitRepeat++;
95
                if ($newFrameAwaitRepeat === $maxFrameAwaitRepeats) {
96
                    $newFrameAwaitRepeat = 0;
97
                    yield [self::TYPE_RELEASE, $socket_last_error, socket_strerror($socket_last_error)];
98
                }
99
                if ($socket_last_error === 35) {
100
                    usleep(self::DEFAULT_TIMEOUT);
101
                    continue;
102
                }
103
                $this->close();
104
                yield [self::TYPE_ERROR, $socket_last_error, socket_strerror($socket_last_error)];
105
                continue;
106
            }
107
108
            $length = unpack('P', $header);
109
            $localBuffer = '';
110
            $bytesToRead = $length[1];
111
            $bytesRead = 0;
112
            //$value = 2 ** ((int) ($bytesToRead / 2));
113
            //socket_set_option($this->socket, SOL_SOCKET, SO_RCVBUF, $value);
114
            $repeat = 0;
115
            while ($bytesRead < $bytesToRead) {
116
                //$buffer = socket_read($this->socket,  $bytesToRead - $bytesRead);
117
                //$bufferLength = strlen($buffer);
118
                $bufferLength = socket_recv($this->socket, $buffer, min($bytesToRead - $bytesRead, self::DEFAULT_BUFFER_SIZE), MSG_DONTWAIT);
119
                if ($bufferLength === false) {
120
                    if ($repeat === $maxRepeats) {
121
                        break;
122
                    }
123
                    //if ($bufferLength === false) {
124
                    $socket_last_error = socket_last_error($this->socket);
125
                    if ($socket_last_error === 35) {
126
                        $repeat++;
127
                        usleep(self::DEFAULT_TIMEOUT * 5);
128
                        continue;
129
                    }
130
                    $this->close();
131
                    break;
132
                }
133
134
                $localBuffer .= $buffer;
135
                $bytesRead += $bufferLength;
136
            }
137
            yield [self::TYPE_RESULT, base64_decode($localBuffer)];
138
        }
139
    }
140
141
    public function broadcast(int $type, string $data): array
142
    {
143
        $files = glob(sys_get_temp_dir() . '/yii-dev-server-*.sock', GLOB_NOSORT);
144
        //echo 'Files: ' . implode(', ', $files) . "\n";
145
        $uniqueErrors = [];
146
        $payload = json_encode([$type, $data], JSON_THROW_ON_ERROR);
147
        foreach ($files as $file) {
148
            $socket = @fsockopen('udg://' . $file, -1, $errno, $errstr);
149
            if ($errno === 61) {
150
                @unlink($file);
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

150
                /** @scrutinizer ignore-unhandled */ @unlink($file);

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...
151
                continue;
152
            }
153
            if ($errno !== 0) {
154
                $uniqueErrors[$errno] = $errstr;
155
                continue;
156
            }
157
            try {
158
                if (!$this->fwriteStream($socket, $payload)) {
0 ignored issues
show
Bug introduced by
It seems like $socket can also be of type false; however, parameter $fp of Yiisoft\Yii\Debug\DebugS...nection::fwriteStream() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

158
                if (!$this->fwriteStream(/** @scrutinizer ignore-type */ $socket, $payload)) {
Loading history...
159
                    $uniqueErrors[] = error_get_last();
160
                    /**
161
                     * Connection is closed.
162
                     */
163
                    continue;
164
                }
165
            } catch (Throwable $e) {
166
                //@unlink($file);
167
                throw $e;
168
            } finally {
169
                //fflush($socket);
170
                fclose($socket);
0 ignored issues
show
Bug introduced by
It seems like $socket can also be of type false; however, parameter $stream of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

170
                fclose(/** @scrutinizer ignore-type */ $socket);
Loading history...
171
            }
172
        }
173
        return $uniqueErrors;
174
    }
175
176
    public function getUri(): string
177
    {
178
        return $this->uri;
179
    }
180
181
    public function close(): void
182
    {
183
        @socket_getsockname($this->socket, $path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for socket_getsockname(). 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

183
        /** @scrutinizer ignore-unhandled */ @socket_getsockname($this->socket, $path);

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...
184
        @socket_close($this->socket);
0 ignored issues
show
Bug introduced by
Are you sure the usage of socket_close($this->socket) is correct as it 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...
Security Best Practice introduced by
It seems like you do not handle an error condition for socket_close(). 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

184
        /** @scrutinizer ignore-unhandled */ @socket_close($this->socket);

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...
185
        @unlink($path);
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

185
        /** @scrutinizer ignore-unhandled */ @unlink($path);

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...
186
    }
187
188
    /**
189
     * @param resource $fp
190
     */
191
    private function fwriteStream($fp, string $data): int|false
192
    {
193
        $data = base64_encode($data);
194
        $strlen = strlen($data);
195
        fwrite($fp, pack('P', $strlen));
196
        for ($written = 0; $written < $strlen; $written += $fwrite) {
197
            $fwrite = fwrite($fp, substr($data, $written), self::DEFAULT_BUFFER_SIZE);
198
            //\fflush($fp);
199
            usleep(self::DEFAULT_TIMEOUT * 5);
200
            if ($fwrite === false) {
201
                return $written;
202
            }
203
        }
204
        return $written;
205
    }
206
}
207