Failed Conditions
Pull Request — master (#1)
by thomas
176:33 queued 175:14
created

src/Node/Server.php (1 issue)

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);
117
118
        if (0 !== $res) {
119
            if (getenv('BITCOINDSERVER_DEBUG_START')) {
120
                echo file_get_contents($this->options->getAbsoluteLogPath($this->config));
121
            }
122
            var_dump($res);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($res) looks like debug code. Are you sure you do not want to remove it?
Loading history...
123
            var_dump($out);
124
            throw new \RuntimeException("Failed to start bitcoind: {$this->options->getDataDir()}\n");
125
        }
126
127
        $tries = 3;
128
        do {
129
            if (!$this->isRunning()) {
130
                if ($tries === 0) {
131
                    if (getenv('BITCOINDSERVER_DEBUG_START')) {
132
                        echo file_get_contents($this->options->getAbsoluteLogPath($this->config));
133
                    }
134
                    throw new \RuntimeException("node didn't start");
135
                }
136
                usleep(50000);
137
            }
138
        } while ($tries-- > 0 && !$this->isRunning());
139
    }
140
141
    /**
142
     * @return Client
143
     * @throws ServerException
144
     * @throws \BitWasp\Bitcoind\Exception\BitcoindException
145
     */
146
    public function getClient(): Client
147
    {
148
        if (!$this->isRunning()) {
149
            throw new ServerException("Cannot get Client for non-running server");
150
        }
151
152
        $client = new Client($this->config->getRpcDsn());
153
        $client->withDriver(new CurlDriver());
154
        return $client;
155
    }
156
157
    public function shutdown()
158
    {
159
        if (!$this->isRunning()) {
160
            throw new ServerException("Server is not running, cannot shut down");
161
        }
162
163
        $out = null;
164
        $ret = null;
165
        exec("kill -15 {$this->getPid()}", $out, $ret);
166
        if ($ret !== 0) {
167
            throw new ServerException("Failed sending SIGTERM to node");
168
        }
169
170
        $timeoutSeconds = 5;
171
        $sleepsPerSecond = 5;
172
        $steps = $timeoutSeconds * $sleepsPerSecond;
173
        $usleep = pow(10, 6) / $sleepsPerSecond;
174
175
        for ($i = 0; $i < $steps; $i++) {
176
            if ($this->isRunning()) {
177
                usleep($usleep);
178
            }
179
        }
180
181
        if ($this->isRunning()) {
182
            throw new ServerException("Failed to shutdown node!");
183
        }
184
    }
185
186
    public function isRunning(): bool
187
    {
188
        return $this->config != null && file_exists($this->options->getAbsolutePidPath($this->config));
189
    }
190
191
    public function getPid(): int
192
    {
193
        if (!$this->isRunning()) {
194
            throw new ServerException("Server is not running - no PID file");
195
        }
196
197
        return (int) trim(file_get_contents($this->options->getAbsolutePidPath($this->config)));
198
    }
199
}
200