Server::start()   C
last analyzed

Complexity

Conditions 8
Paths 7

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 17.4958

Importance

Changes 0
Metric Value
cc 8
eloc 19
nc 7
nop 1
dl 0
loc 29
ccs 8
cts 17
cp 0.4706
crap 17.4958
rs 5.3846
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
use Nbobtc\Http\Driver\DriverInterface;
14
15
class Server
16
{
17
    const ERROR_STARTUP = -28;
18
    const ERROR_TX_MEMPOOL_CONFLICT = -26;
19
20
    /**
21
     * @var NodeOptions
22
     */
23
    private $options;
24
25
    /**
26
     * @var Config
27
     */
28
    private $config;
29
30
    /**
31
     * Server constructor.
32
     * @param NodeOptions $options
33 8
     */
34
    public function __construct(NodeOptions $options)
35 8
    {
36 1
        if (!is_dir($options->getDataDir())) {
37
            throw new ServerException("Cannot create server without a valid datadir");
38 7
        }
39 7
        $this->options = $options;
40
    }
41
42
    public function getNodeOptions(): NodeOptions
43
    {
44
        return $this->options;
45
    }
46
47
    private function secondsToMicro(float $seconds): int
48
    {
49
        return (int) $seconds * 1000000;
50
    }
51
52
    /**
53
     * @return bool
54 2
     */
55
    public function waitForStartup(): bool
56 2
    {
57 2
        for ($i = 0; $i < 5; $i++) {
58 2
            if (file_exists($this->options->getAbsolutePidPath($this->config))) {
59
                return true;
60
            }
61
            sleep(1);
62
        }
63
        return false;
64
    }
65 1
66
    public function waitForRpc(): bool
67 1
    {
68 1
        $start = microtime(true);
69 1
        $limit = 10;
70
        $connected = false;
71 1
72
        $conn = $this->getClient();
73
        do {
74 1
            try {
75 1
                $result = json_decode($conn->sendCommand(new Command("getblockchaininfo"))->getBody()->getContents(), true);
76 1
                if ($result['error'] === null) {
77
                    $connected = true;
78
                } else {
79
                    if ($result['error']['code'] !== self::ERROR_STARTUP) {
80
                        throw new \RuntimeException("Unexpected error code during startup");
81
                    }
82 1
83
                    usleep($this->secondsToMicro(0.02));
84
                }
85
            } catch (\Exception $e) {
86
                usleep($this->secondsToMicro(0.02));
87
            }
88 1
89
            if (microtime(true) > $start + $limit) {
90
                throw new \RuntimeException("Timeout elapsed, never made connection to bitcoind");
91 1
            }
92
        } while (!$connected);
93 1
94
        return $connected;
95
    }
96 2
97
    public function getConfig(ConfigLoader $loader): Config
98 2
    {
99 2
        return $loader->load(
100
            $this->options->getAbsoluteConfigPath()
101
        );
102
    }
103
104
    /**
105
     * @param ConfigLoader $loader
106 2
     */
107
    public function start(ConfigLoader $loader)
108 2
    {
109
        if ($this->isRunning()) {
110
            return;
111
        }
112 2
113
        $this->config = $this->getConfig($loader);
114 2
115 2
        $res = null;
116 2
        $out = [];
117
        exec($this->options->getStartupCommand(), $out, $res);
118 2
119
        if (0 !== $res) {
120
            if (getenv('BITCOINDSERVER_DEBUG_START')) {
121
                echo file_get_contents($this->options->getAbsoluteLogPath($this->config));
122
            }
123
            throw new \RuntimeException("Failed to start bitcoind: {$this->options->getDataDir()}\n");
124
        }
125 2
126
        $tries = 3;
127 2
        do {
128
            if (!$this->isRunning()) {
129
                if ($tries === 0) {
130
                    if (getenv('BITCOINDSERVER_DEBUG_START')) {
131
                        echo file_get_contents($this->options->getAbsoluteLogPath($this->config));
132
                    }
133
                    throw new \RuntimeException("node didn't start");
134
                }
135
                usleep(50000);
136 2
            }
137 2
        } while ($tries-- > 0 && !$this->isRunning());
138
    }
139
140
    /**
141
     * @return Client
142
     * @throws ServerException
143
     * @throws \BitWasp\Bitcoind\Exception\BitcoindException
144 2
     */
145
    public function getClient(DriverInterface $driver = null): Client
146 2
    {
147 1
        if (!$this->isRunning()) {
148
            throw new ServerException("Cannot get Client for non-running server");
149
        }
150 1
151 1
        $client = new Client($this->config->getRpcDsn());
152 1
        $client->withDriver($driver ?: new CurlDriver());
153
        return $client;
154
    }
155 3
156
    public function shutdown()
157 3
    {
158 1
        if (!$this->isRunning()) {
159
            throw new ServerException("Server is not running, cannot shut down");
160
        }
161 2
162 2
        $out = null;
163 2
        $ret = null;
164 2
        exec("kill -15 {$this->getPid()}", $out, $ret);
165
        if ($ret !== 0) {
166
            throw new ServerException("Failed sending SIGTERM to node");
167
        }
168 2
169 2
        $timeoutSeconds = 5;
170 2
        $sleepsPerSecond = 5;
171 2
        $steps = $timeoutSeconds * $sleepsPerSecond;
172
        $usleep = pow(10, 6) / $sleepsPerSecond;
173 2
174 2
        for ($i = 0; $i < $steps; $i++) {
175 2
            if ($this->isRunning()) {
176
                usleep($usleep);
177
            }
178
        }
179 2
180
        if ($this->isRunning()) {
181
            throw new ServerException("Failed to shutdown node!");
182 2
        }
183
    }
184 5
185
    public function isRunning(): bool
186 5
    {
187
        return $this->config != null && file_exists($this->options->getAbsolutePidPath($this->config));
188
    }
189 3
190
    public function getPid(): int
191 3
    {
192 1
        if (!$this->isRunning()) {
193
            throw new ServerException("Server is not running - no PID file");
194
        }
195 2
196
        return (int) trim(file_get_contents($this->options->getAbsolutePidPath($this->config)));
197
    }
198
}
199