Completed
Push — master ( 169667...efb279 )
by thomas
11s
created

Server::getNodeOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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