Failed Conditions
Push — master ( ee09ca...254dbe )
by thomas
97:03 queued 67:04
created

Server::waitForRpc()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 14
nop 0
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoind\Node;
6
7
use BitWasp\Bitcoind\Config\Config;
8
use BitWasp\Bitcoind\Config\Loader as ConfigLoader;
9
use BitWasp\Bitcoind\Exception\ServerException;
10
use BitWasp\Bitcoind\HttpDriver\CurlDriver;
11
use Nbobtc\Command\Command;
12
use Nbobtc\Http\Client;
13
14
class Server
15
{
16
    const ERROR_STARTUP = -28;
17
    const ERROR_TX_MEMPOOL_CONFLICT = -26;
18
19
    /**
20
     * @var NodeOptions
21
     */
22
    private $options;
23
24
    /**
25
     * @var Config
26
     */
27
    private $config;
28
29
    /**
30
     * Server constructor.
31
     * @param NodeOptions $options
32
     */
33
    public function __construct(NodeOptions $options)
34
    {
35
        if (!is_dir($options->getDataDir())) {
36
            throw new ServerException("Cannot create server without a valid datadir");
37
        }
38
        $this->options = $options;
39
    }
40
41
    public function getNodeOptions(): NodeOptions
42
    {
43
        return $this->options;
44
    }
45
46
    private function secondsToMicro(float $seconds): int
47
    {
48
        return (int) $seconds * 1000000;
49
    }
50
51
    /**
52
     * @return bool
53
     */
54
    public function waitForStartup(): bool
55
    {
56
        for ($i = 0; $i < 5; $i++) {
57
            if (file_exists($this->options->getAbsolutePidPath($this->config))) {
58
                return true;
59
            }
60
            sleep(1);
61
        }
62
        return false;
63
    }
64
65
    public function waitForRpc(): bool
66
    {
67
        $start = microtime(true);
68
        $limit = 10;
69
        $connected = false;
70
71
        $conn = $this->getClient();
72
        do {
73
            try {
74
                $result = json_decode($conn->sendCommand(new Command("getblockchaininfo"))->getBody()->getContents(), true);
75
                if ($result['error'] === null) {
76
                    $connected = true;
77
                } else {
78
                    if ($result['error']['code'] !== self::ERROR_STARTUP) {
79
                        throw new \RuntimeException("Unexpected error code during startup");
80
                    }
81
82
                    usleep($this->secondsToMicro(0.02));
83
                }
84
            } catch (\Exception $e) {
85
                usleep($this->secondsToMicro(0.02));
86
            }
87
88
            if (microtime(true) > $start + $limit) {
89
                throw new \RuntimeException("Timeout elapsed, never made connection to bitcoind");
90
            }
91
        } while (!$connected);
92
93
        return $connected;
94
    }
95
96
    public function getConfig(ConfigLoader $loader): Config
97
    {
98
        return $loader->load(
99
            $this->options->getAbsoluteConfigPath()
100
        );
101
    }
102
103
    /**
104
     * @param ConfigLoader $loader
105
     */
106
    public function start(ConfigLoader $loader)
107
    {
108
        if ($this->isRunning()) {
109
            return;
110
        }
111
112
        $this->config = $this->getConfig($loader);
113
114
        $res = 0;
115
        $out = '';
116
        exec($this->options->getStartupCommand(), $out, $res);
0 ignored issues
show
Bug introduced by
$out of type string is incompatible with the type null|array expected by parameter $output of exec(). ( Ignorable by Annotation )

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

116
        exec($this->options->getStartupCommand(), /** @scrutinizer ignore-type */ $out, $res);
Loading history...
117
118
        if (0 !== $res) {
0 ignored issues
show
introduced by
The condition 0 !== $res is always false.
Loading history...
119
            throw new \RuntimeException("Failed to start bitcoind: {$this->options->getDataDir()}\n");
120
        }
121
122
        $tries = 3;
123
        do {
124
            if (!$this->isRunning()) {
125
                if ($tries === 0) {
126
                    if (getenv('BITCOINDSERVER_DEBUG_START')) {
127
                        print_r(file_get_contents($this->options->getAbsoluteLogPath($this->config)));
128
                    }
129
                    throw new \RuntimeException("node didn't start");
130
                }
131
                usleep(0.05 * 1e5);
0 ignored issues
show
Bug introduced by
0.05 * 100000.0 of type double is incompatible with the type integer expected by parameter $micro_seconds of usleep(). ( Ignorable by Annotation )

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

131
                usleep(/** @scrutinizer ignore-type */ 0.05 * 1e5);
Loading history...
132
            }
133
        } while($tries-- > 0 && !$this->isRunning());
134
    }
135
136
    /**
137
     * @return Client
138
     * @throws ServerException
139
     * @throws \BitWasp\Bitcoind\Exception\BitcoindException
140
     */
141
    public function getClient(): Client
142
    {
143
        if (!$this->isRunning()) {
144
            throw new ServerException("Cannot get Client for non-running server");
145
        }
146
147
        $client = new Client($this->config->getRpcDsn());
148
        $client->withDriver(new CurlDriver());
149
        return $client;
150
    }
151
152
    public function shutdown()
153
    {
154
        if (!$this->isRunning()) {
155
            throw new ServerException("Server is not running, cannot shut down");
156
        }
157
158
        $out = null;
159
        $ret = null;
160
        exec("kill -15 {$this->getPid()}", $out, $ret);
161
        if ($ret !== 0) {
0 ignored issues
show
introduced by
The condition $ret !== 0 is always true.
Loading history...
162
            throw new ServerException("Failed sending SIGTERM to node");
163
        }
164
165
        $timeoutSeconds = 5;
166
        $sleepsPerSecond = 5;
167
        $steps = $timeoutSeconds * $sleepsPerSecond;
168
        $usleep = pow(10, 6) / $sleepsPerSecond;
169
170
        for ($i = 0; $i < $steps; $i++) {
171
            if ($this->isRunning()) {
172
                usleep($usleep);
173
            }
174
        }
175
176
        if ($this->isRunning()) {
177
            throw new ServerException("Failed to shutdown node!");
178
        }
179
    }
180
181
    public function isRunning(): bool
182
    {
183
        return $this->config != null && file_exists($this->options->getAbsolutePidPath($this->config));
184
    }
185
186
    public function getPid(): int
187
    {
188
        if (!$this->isRunning()) {
189
            throw new ServerException("Server is not running - no PID file");
190
        }
191
192
        return (int) trim(file_get_contents($this->options->getAbsolutePidPath($this->config)));
193
    }
194
}
195