Passed
Pull Request — master (#225)
by Dmitriy
02:36
created

Connection::read()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 24
nc 7
nop 0
dl 0
loc 33
rs 8.6026
c 0
b 0
f 0
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
25
    public const MESSAGE_TYPE_VAR_DUMPER = 0x001B;
26
    public const MESSAGE_TYPE_LOGGER = 0x002B;
27
28
    private string $uri;
29
30
    public function __construct(
31
        private Socket $socket,
32
    ) {
33
    }
34
35
    public static function create(): self
36
    {
37
        $socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
38
39
        $socket_last_error = socket_last_error($socket);
40
41
        if ($socket_last_error) {
42
            throw new RuntimeException(
43
                sprintf(
44
                    '"socket_last_error" returned %d: "%s".',
45
                    $socket_last_error,
46
                    socket_strerror($socket_last_error),
47
                ),
48
            );
49
        }
50
51
        return new self(
52
            $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

52
            /** @scrutinizer ignore-type */ $socket,
Loading history...
53
        );
54
    }
55
56
    public function bind(): void
57
    {
58
        $n = random_int(0, PHP_INT_MAX);
59
        $file = sprintf(sys_get_temp_dir() . '/yii-dev-server-%d.sock', $n);
60
        $this->uri = $file;
61
        if (!socket_bind($this->socket, $file)) {
62
            $socket_last_error = socket_last_error($this->socket);
63
64
            throw new RuntimeException(
65
                sprintf(
66
                    'An error occurred while reading the socket. "socket_last_error" returned %d: "%s".',
67
                    $socket_last_error,
68
                    socket_strerror($socket_last_error),
69
                ),
70
            );
71
        }
72
    }
73
74
    /**
75
     * @return Generator<int, array{0: self::TYPE_ERROR|self::TYPE_RESULT, 1: string, 2: int|string, 3?: int}>
76
     */
77
    public function read(): Generator
78
    {
79
        while (true) {
80
            if (!socket_recvfrom($this->socket, $header, self::DEFAULT_BUFFER_SIZE, MSG_DONTWAIT, $ip, $port)) {
81
                $socket_last_error = socket_last_error($this->socket);
82
                if ($socket_last_error === 35) {
83
                    usleep(self::DEFAULT_TIMEOUT);
84
                    continue;
85
                }
86
                $this->close();
87
                yield [self::TYPE_ERROR, $socket_last_error, socket_strerror($socket_last_error)];
88
                continue;
89
            }
90
91
            $length = unpack('N', $header);
92
            $localBuffer = '';
93
            $bytesToRead = $length[1];
94
            $bytesRead = 0;
95
            while ($bytesRead < $bytesToRead) {
96
                if (!$bufferLength = socket_recvfrom($this->socket, $buffer, self::DEFAULT_BUFFER_SIZE, MSG_DONTWAIT, $ip, $port)) {
97
                    $socket_last_error = socket_last_error($this->socket);
98
                    if ($socket_last_error === 35) {
99
                        usleep(self::DEFAULT_TIMEOUT);
100
                        continue;
101
                    }
102
                    $this->close();
103
                    break;
104
                }
105
106
                $localBuffer .= $buffer;
107
                $bytesRead += $bufferLength;
108
            }
109
            yield [self::TYPE_RESULT, base64_decode($localBuffer), $ip, $port];
110
        }
111
    }
112
113
    public function broadcast(int $type, string $data): array
114
    {
115
        $files = glob(sys_get_temp_dir() . '/yii-dev-server-*.sock', GLOB_NOSORT);
116
        //echo 'Files: ' . implode(', ', $files) . "\n";
117
        $uniqueErrors = [];
118
        $payload = base64_encode(json_encode([$type, $data], JSON_THROW_ON_ERROR));
119
        $payloadLength = strlen($payload);
0 ignored issues
show
Unused Code introduced by
The assignment to $payloadLength is dead and can be removed.
Loading history...
120
        foreach ($files as $file) {
121
            $socket = @fsockopen('udg://' . $file, -1, $errno, $errstr);
122
            if ($errno === 61) {
123
                @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

123
                /** @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...
124
                continue;
125
            }
126
            if ($errno !== 0) {
127
                $uniqueErrors[$errno] = $errstr;
128
                continue;
129
            }
130
            try {
131
                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

131
                if (!$this->fwriteStream(/** @scrutinizer ignore-type */ $socket, $payload)) {
Loading history...
132
                    $uniqueErrors[] = error_get_last();
133
                    /**
134
                     * Connection is closed.
135
                     */
136
                    continue;
137
                }
138
            } catch (Throwable $e) {
139
                //@unlink($file);
140
                throw $e;
141
            } finally {
142
                //fflush($socket);
143
                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

143
                fclose(/** @scrutinizer ignore-type */ $socket);
Loading history...
144
            }
145
        }
146
        return $uniqueErrors;
147
    }
148
149
    public function getUri(): string
150
    {
151
        return $this->uri;
152
    }
153
154
    public function close(): void
155
    {
156
        @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

156
        /** @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...
157
        @socket_close($this->socket);
0 ignored issues
show
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

157
        /** @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...
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...
158
        @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

158
        /** @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...
159
    }
160
161
    /**
162
     * @param resource $fp
163
     */
164
    private function fwriteStream($fp, string $data): int|false
165
    {
166
        $strlen = strlen($data);
167
        fwrite($fp, pack('N', $strlen));
168
        for ($written = 0; $written < $strlen; $written += $fwrite) {
169
            $fwrite = fwrite($fp, substr($data, $written), self::DEFAULT_BUFFER_SIZE);
170
            //\fflush($fp);
171
            usleep(self::DEFAULT_TIMEOUT / 2);
172
            if ($fwrite === false) {
173
                return $written;
174
            }
175
        }
176
        return $written;
177
    }
178
}
179