/tmp/bitcoin/src/rpc/node.cpp
Line | Count | Source |
1 | | // Copyright (c) 2010 Satoshi Nakamoto |
2 | | // Copyright (c) 2009-present The Bitcoin Core developers |
3 | | // Distributed under the MIT software license, see the accompanying |
4 | | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
5 | | |
6 | | #include <bitcoin-build-config.h> // IWYU pragma: keep |
7 | | |
8 | | #include <chainparams.h> |
9 | | #include <httpserver.h> |
10 | | #include <index/blockfilterindex.h> |
11 | | #include <index/coinstatsindex.h> |
12 | | #include <index/txindex.h> |
13 | | #include <index/txospenderindex.h> |
14 | | #include <interfaces/chain.h> |
15 | | #include <interfaces/echo.h> |
16 | | #include <interfaces/init.h> |
17 | | #include <interfaces/ipc.h> |
18 | | #include <kernel/cs_main.h> |
19 | | #include <logging.h> |
20 | | #include <node/context.h> |
21 | | #include <rpc/server.h> |
22 | | #include <rpc/server_util.h> |
23 | | #include <rpc/util.h> |
24 | | #include <scheduler.h> |
25 | | #include <tinyformat.h> |
26 | | #include <univalue.h> |
27 | | #include <util/any.h> |
28 | | #include <util/check.h> |
29 | | #include <util/time.h> |
30 | | |
31 | | #include <cstdint> |
32 | | #ifdef HAVE_MALLOC_INFO |
33 | | #include <malloc.h> |
34 | | #endif |
35 | | #include <string_view> |
36 | | |
37 | | using node::NodeContext; |
38 | | |
39 | | static RPCMethod setmocktime() |
40 | 3.66k | { |
41 | 3.66k | return RPCMethod{ |
42 | 3.66k | "setmocktime", |
43 | 3.66k | "Set the local time to given timestamp (-regtest only)\n", |
44 | 3.66k | { |
45 | 3.66k | {"timestamp", RPCArg::Type::NUM, RPCArg::Optional::NO, UNIX_EPOCH_TIME + "\n" |
46 | 3.66k | "Pass 0 to go back to using the system time."}, |
47 | 3.66k | }, |
48 | 3.66k | RPCResult{RPCResult::Type::NONE, "", ""}, |
49 | 3.66k | RPCExamples{""}, |
50 | 3.66k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
51 | 3.66k | { |
52 | 1.36k | if (!Params().IsMockableChain()) { |
53 | 0 | throw std::runtime_error("setmocktime is for regression testing (-regtest mode) only"); |
54 | 0 | } |
55 | | |
56 | | // For now, don't change mocktime if we're in the middle of validation, as |
57 | | // this could have an effect on mempool time-based eviction, as well as |
58 | | // IsCurrentForFeeEstimation() and IsInitialBlockDownload(). |
59 | | // TODO: figure out the right way to synchronize around mocktime, and |
60 | | // ensure all call sites of GetTime() are accessing this safely. |
61 | 1.36k | LOCK(cs_main); |
62 | | |
63 | 1.36k | const int64_t time{request.params[0].getInt<int64_t>()}; |
64 | 1.36k | constexpr int64_t max_time{Ticks<std::chrono::seconds>(std::chrono::nanoseconds::max())}; |
65 | 1.36k | if (time < 0 || time > max_time) { |
66 | 1 | throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Mocktime must be in the range [0, %s], not %s.", max_time, time)); |
67 | 1 | } |
68 | | |
69 | 1.36k | SetMockTime(time); |
70 | 1.36k | const NodeContext& node_context{EnsureAnyNodeContext(request.context)}; |
71 | 1.36k | for (const auto& chain_client : node_context.chain_clients) { |
72 | 168 | chain_client->setMockTime(time); |
73 | 168 | } |
74 | | |
75 | 1.36k | return UniValue::VNULL; |
76 | 1.36k | }, |
77 | 3.66k | }; |
78 | 3.66k | } |
79 | | |
80 | | static RPCMethod mockscheduler() |
81 | 2.31k | { |
82 | 2.31k | return RPCMethod{ |
83 | 2.31k | "mockscheduler", |
84 | 2.31k | "Bump the scheduler into the future (-regtest only)\n", |
85 | 2.31k | { |
86 | 2.31k | {"delta_time", RPCArg::Type::NUM, RPCArg::Optional::NO, "Number of seconds to forward the scheduler into the future." }, |
87 | 2.31k | }, |
88 | 2.31k | RPCResult{RPCResult::Type::NONE, "", ""}, |
89 | 2.31k | RPCExamples{""}, |
90 | 2.31k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
91 | 2.31k | { |
92 | 12 | if (!Params().IsMockableChain()) { |
93 | 0 | throw std::runtime_error("mockscheduler is for regression testing (-regtest mode) only"); |
94 | 0 | } |
95 | | |
96 | 12 | int64_t delta_seconds = request.params[0].getInt<int64_t>(); |
97 | 12 | if (delta_seconds <= 0 || delta_seconds > 3600) { |
98 | 0 | throw std::runtime_error("delta_time must be between 1 and 3600 seconds (1 hr)"); |
99 | 0 | } |
100 | | |
101 | 12 | const NodeContext& node_context{EnsureAnyNodeContext(request.context)}; |
102 | 12 | CHECK_NONFATAL(node_context.scheduler)->MockForward(std::chrono::seconds{delta_seconds}); |
103 | 12 | CHECK_NONFATAL(node_context.validation_signals)->SyncWithValidationInterfaceQueue(); |
104 | 12 | for (const auto& chain_client : node_context.chain_clients) { |
105 | 7 | chain_client->schedulerMockForward(std::chrono::seconds(delta_seconds)); |
106 | 7 | } |
107 | | |
108 | 12 | return UniValue::VNULL; |
109 | 12 | }, |
110 | 2.31k | }; |
111 | 2.31k | } |
112 | | |
113 | | static UniValue RPCLockedMemoryInfo() |
114 | 1 | { |
115 | 1 | LockedPool::Stats stats = LockedPoolManager::Instance().stats(); |
116 | 1 | UniValue obj(UniValue::VOBJ); |
117 | 1 | obj.pushKV("used", stats.used); |
118 | 1 | obj.pushKV("free", stats.free); |
119 | 1 | obj.pushKV("total", stats.total); |
120 | 1 | obj.pushKV("locked", stats.locked); |
121 | 1 | obj.pushKV("chunks_used", stats.chunks_used); |
122 | 1 | obj.pushKV("chunks_free", stats.chunks_free); |
123 | 1 | return obj; |
124 | 1 | } |
125 | | |
126 | | #ifdef HAVE_MALLOC_INFO |
127 | | static std::string RPCMallocInfo() |
128 | 1 | { |
129 | 1 | char *ptr = nullptr; |
130 | 1 | size_t size = 0; |
131 | 1 | FILE *f = open_memstream(&ptr, &size); |
132 | 1 | if (f) { |
133 | 1 | malloc_info(0, f); |
134 | 1 | fclose(f); |
135 | 1 | if (ptr) { |
136 | 1 | std::string rv(ptr, size); |
137 | 1 | free(ptr); |
138 | 1 | return rv; |
139 | 1 | } |
140 | 1 | } |
141 | 0 | return ""; |
142 | 1 | } |
143 | | #endif |
144 | | |
145 | | static RPCMethod getmemoryinfo() |
146 | 2.31k | { |
147 | | /* Please, avoid using the word "pool" here in the RPC interface or help, |
148 | | * as users will undoubtedly confuse it with the other "memory pool" |
149 | | */ |
150 | 2.31k | return RPCMethod{"getmemoryinfo", |
151 | 2.31k | "Returns an object containing information about memory usage.\n", |
152 | 2.31k | { |
153 | 2.31k | {"mode", RPCArg::Type::STR, RPCArg::Default{"stats"}, "determines what kind of information is returned.\n" |
154 | 2.31k | " - \"stats\" returns general statistics about memory usage in the daemon.\n" |
155 | 2.31k | " - \"mallocinfo\" returns an XML string describing low-level heap state (only available if compiled with glibc)."}, |
156 | 2.31k | }, |
157 | 2.31k | { |
158 | 2.31k | RPCResult{"mode \"stats\"", |
159 | 2.31k | RPCResult::Type::OBJ, "", "", |
160 | 2.31k | { |
161 | 2.31k | {RPCResult::Type::OBJ, "locked", "Information about locked memory manager", |
162 | 2.31k | { |
163 | 2.31k | {RPCResult::Type::NUM, "used", "Number of bytes used"}, |
164 | 2.31k | {RPCResult::Type::NUM, "free", "Number of bytes available in current arenas"}, |
165 | 2.31k | {RPCResult::Type::NUM, "total", "Total number of bytes managed"}, |
166 | 2.31k | {RPCResult::Type::NUM, "locked", "Amount of bytes that succeeded locking. If this number is smaller than total, locking pages failed at some point and key data could be swapped to disk."}, |
167 | 2.31k | {RPCResult::Type::NUM, "chunks_used", "Number allocated chunks"}, |
168 | 2.31k | {RPCResult::Type::NUM, "chunks_free", "Number unused chunks"}, |
169 | 2.31k | }}, |
170 | 2.31k | } |
171 | 2.31k | }, |
172 | 2.31k | RPCResult{"mode \"mallocinfo\"", |
173 | 2.31k | RPCResult::Type::STR, "", "\"<malloc version=\"1\">...\"" |
174 | 2.31k | }, |
175 | 2.31k | }, |
176 | 2.31k | RPCExamples{ |
177 | 2.31k | HelpExampleCli("getmemoryinfo", "") |
178 | 2.31k | + HelpExampleRpc("getmemoryinfo", "") |
179 | 2.31k | }, |
180 | 2.31k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
181 | 2.31k | { |
182 | 3 | auto mode{self.Arg<std::string_view>("mode")}; |
183 | 3 | if (mode == "stats") { |
184 | 1 | UniValue obj(UniValue::VOBJ); |
185 | 1 | obj.pushKV("locked", RPCLockedMemoryInfo()); |
186 | 1 | return obj; |
187 | 2 | } else if (mode == "mallocinfo") { |
188 | 1 | #ifdef HAVE_MALLOC_INFO |
189 | 1 | return RPCMallocInfo(); |
190 | | #else |
191 | | throw JSONRPCError(RPC_INVALID_PARAMETER, "mallocinfo mode not available"); |
192 | | #endif |
193 | 1 | } else { |
194 | 1 | throw JSONRPCError(RPC_INVALID_PARAMETER, tfm::format("unknown mode %s", mode)); |
195 | 1 | } |
196 | 3 | }, |
197 | 2.31k | }; |
198 | 2.31k | } |
199 | | |
200 | 2 | static void EnableOrDisableLogCategories(UniValue cats, bool enable) { |
201 | 2 | cats = cats.get_array(); |
202 | 4 | for (unsigned int i = 0; i < cats.size(); ++i) { |
203 | 2 | std::string cat = cats[i].get_str(); |
204 | | |
205 | 2 | bool success; |
206 | 2 | if (enable) { |
207 | 1 | success = LogInstance().EnableCategory(cat); |
208 | 1 | } else { |
209 | 1 | success = LogInstance().DisableCategory(cat); |
210 | 1 | } |
211 | | |
212 | 2 | if (!success) { |
213 | 0 | throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown logging category " + cat); |
214 | 0 | } |
215 | 2 | } |
216 | 2 | } |
217 | | |
218 | | static RPCMethod logging() |
219 | 2.32k | { |
220 | 2.32k | return RPCMethod{"logging", |
221 | 2.32k | "Gets and sets the logging configuration.\n" |
222 | 2.32k | "When called without an argument, returns the list of categories with status that are currently being debug logged or not.\n" |
223 | 2.32k | "When called with arguments, adds or removes categories from debug logging and return the lists above.\n" |
224 | 2.32k | "The arguments are evaluated in order \"include\", \"exclude\".\n" |
225 | 2.32k | "If an item is both included and excluded, it will thus end up being excluded.\n" |
226 | 2.32k | "The valid logging categories are: " + LogInstance().LogCategoriesString() + "\n" |
227 | 2.32k | "In addition, the following are available as category names with special meanings:\n" |
228 | 2.32k | " - \"all\", \"1\" : represent all logging categories.\n" |
229 | 2.32k | , |
230 | 2.32k | { |
231 | 2.32k | {"include", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The categories to add to debug logging", |
232 | 2.32k | { |
233 | 2.32k | {"include_category", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "the valid logging category"}, |
234 | 2.32k | }}, |
235 | 2.32k | {"exclude", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The categories to remove from debug logging", |
236 | 2.32k | { |
237 | 2.32k | {"exclude_category", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "the valid logging category"}, |
238 | 2.32k | }}, |
239 | 2.32k | }, |
240 | 2.32k | RPCResult{ |
241 | 2.32k | RPCResult::Type::OBJ_DYN, "", "keys are the logging categories, and values indicates its status", |
242 | 2.32k | { |
243 | 2.32k | {RPCResult::Type::BOOL, "category", "if being debug logged or not. false:inactive, true:active"}, |
244 | 2.32k | } |
245 | 2.32k | }, |
246 | 2.32k | RPCExamples{ |
247 | 2.32k | HelpExampleCli("logging", "\"[\\\"all\\\"]\" \"[\\\"http\\\"]\"") |
248 | 2.32k | + HelpExampleRpc("logging", "[\"all\"], [\"libevent\"]") |
249 | 2.32k | }, |
250 | 2.32k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
251 | 2.32k | { |
252 | 10 | BCLog::CategoryMask original_log_categories = LogInstance().GetCategoryMask(); |
253 | 10 | if (request.params[0].isArray()) { |
254 | 1 | EnableOrDisableLogCategories(request.params[0], true); |
255 | 1 | } |
256 | 10 | if (request.params[1].isArray()) { |
257 | 1 | EnableOrDisableLogCategories(request.params[1], false); |
258 | 1 | } |
259 | 10 | BCLog::CategoryMask updated_log_categories = LogInstance().GetCategoryMask(); |
260 | 10 | BCLog::CategoryMask changed_log_categories = original_log_categories ^ updated_log_categories; |
261 | | |
262 | | // Update libevent logging if BCLog::LIBEVENT has changed. |
263 | 10 | if (changed_log_categories & BCLog::LIBEVENT) { |
264 | 0 | UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); |
265 | 0 | } |
266 | | |
267 | 10 | UniValue result(UniValue::VOBJ); |
268 | 310 | for (const auto& logCatActive : LogInstance().LogCategoriesList()) { |
269 | 310 | result.pushKV(logCatActive.category, logCatActive.active); |
270 | 310 | } |
271 | | |
272 | 10 | return result; |
273 | 10 | }, |
274 | 2.32k | }; |
275 | 2.32k | } |
276 | | |
277 | | static RPCMethod echo(const std::string& name) |
278 | 4.63k | { |
279 | 4.63k | return RPCMethod{ |
280 | 4.63k | name, |
281 | 4.63k | "Simply echo back the input arguments. This command is for testing.\n" |
282 | 4.63k | "\nIt will return an internal bug report when arg9='trigger_internal_bug' is passed.\n" |
283 | 4.63k | "\nThe difference between echo and echojson is that echojson has argument conversion enabled in the client-side table in " |
284 | 4.63k | "bitcoin-cli and the GUI. There is no server-side difference.", |
285 | 4.63k | { |
286 | 4.63k | {"arg0", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
287 | 4.63k | {"arg1", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
288 | 4.63k | {"arg2", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
289 | 4.63k | {"arg3", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
290 | 4.63k | {"arg4", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
291 | 4.63k | {"arg5", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
292 | 4.63k | {"arg6", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
293 | 4.63k | {"arg7", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
294 | 4.63k | {"arg8", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
295 | 4.63k | {"arg9", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.skip_type_check = true}}, |
296 | 4.63k | }, |
297 | 4.63k | RPCResult{RPCResult::Type::ANY, "", "Returns whatever was passed in"}, |
298 | 4.63k | RPCExamples{""}, |
299 | 4.63k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
300 | 4.63k | { |
301 | 22 | if (request.params[9].isStr()) { |
302 | 0 | CHECK_NONFATAL(request.params[9].get_str() != "trigger_internal_bug"); |
303 | 0 | } |
304 | | |
305 | 22 | return request.params; |
306 | 22 | }, |
307 | 4.63k | }; |
308 | 4.63k | } |
309 | | |
310 | 2.32k | static RPCMethod echo() { return echo("echo"); } |
311 | 2.30k | static RPCMethod echojson() { return echo("echojson"); } |
312 | | |
313 | | static RPCMethod echoipc() |
314 | 2.30k | { |
315 | 2.30k | return RPCMethod{ |
316 | 2.30k | "echoipc", |
317 | 2.30k | "Echo back the input argument, passing it through a spawned process in a multiprocess build.\n" |
318 | 2.30k | "This command is for testing.\n", |
319 | 2.30k | {{"arg", RPCArg::Type::STR, RPCArg::Optional::NO, "The string to echo",}}, |
320 | 2.30k | RPCResult{RPCResult::Type::STR, "echo", "The echoed string."}, |
321 | 2.30k | RPCExamples{HelpExampleCli("echo", "\"Hello world\"") + |
322 | 2.30k | HelpExampleRpc("echo", "\"Hello world\"")}, |
323 | 2.30k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue { |
324 | 1 | interfaces::Init& local_init = *EnsureAnyNodeContext(request.context).init; |
325 | 1 | std::unique_ptr<interfaces::Echo> echo; |
326 | 1 | if (interfaces::Ipc* ipc = local_init.ipc()) { |
327 | | // Spawn a new bitcoin-node process and call makeEcho to get a |
328 | | // client pointer to a interfaces::Echo instance running in |
329 | | // that process. This is just for testing. A slightly more |
330 | | // realistic test spawning a different executable instead of |
331 | | // the same executable would add a new bitcoin-echo executable, |
332 | | // and spawn bitcoin-echo below instead of bitcoin-node. But |
333 | | // using bitcoin-node avoids the need to build and install a |
334 | | // new executable just for this one test. |
335 | 0 | auto init = ipc->spawnProcess("bitcoin-node"); |
336 | 0 | echo = init->makeEcho(); |
337 | 0 | ipc->addCleanup(*echo, [init = init.release()] { delete init; }); |
338 | 1 | } else { |
339 | | // IPC support is not available because this is a bitcoind |
340 | | // process not a bitcoind-node process, so just create a local |
341 | | // interfaces::Echo object and return it so the `echoipc` RPC |
342 | | // method will work, and the python test calling `echoipc` |
343 | | // can expect the same result. |
344 | 1 | echo = local_init.makeEcho(); |
345 | 1 | } |
346 | 1 | return echo->echo(request.params[0].get_str()); |
347 | 1 | }, |
348 | 2.30k | }; |
349 | 2.30k | } |
350 | | |
351 | | static UniValue SummaryToJSON(const IndexSummary&& summary, std::string index_name) |
352 | 165 | { |
353 | 165 | UniValue ret_summary(UniValue::VOBJ); |
354 | 165 | if (!index_name.empty() && index_name != summary.name) return ret_summary; |
355 | | |
356 | 149 | UniValue entry(UniValue::VOBJ); |
357 | 149 | entry.pushKV("synced", summary.synced); |
358 | 149 | entry.pushKV("best_block_height", summary.best_block_height); |
359 | 149 | ret_summary.pushKV(summary.name, std::move(entry)); |
360 | 149 | return ret_summary; |
361 | 165 | } |
362 | | |
363 | | static RPCMethod getindexinfo() |
364 | 2.37k | { |
365 | 2.37k | return RPCMethod{ |
366 | 2.37k | "getindexinfo", |
367 | 2.37k | "Returns the status of one or all available indices currently running in the node.\n", |
368 | 2.37k | { |
369 | 2.37k | {"index_name", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Filter results for an index with a specific name."}, |
370 | 2.37k | }, |
371 | 2.37k | RPCResult{ |
372 | 2.37k | RPCResult::Type::OBJ_DYN, "", "", { |
373 | 2.37k | { |
374 | 2.37k | RPCResult::Type::OBJ, "name", "The name of the index", |
375 | 2.37k | { |
376 | 2.37k | {RPCResult::Type::BOOL, "synced", "Whether the index is synced or not"}, |
377 | 2.37k | {RPCResult::Type::NUM, "best_block_height", "The block height to which the index is synced"}, |
378 | 2.37k | } |
379 | 2.37k | }, |
380 | 2.37k | }, |
381 | 2.37k | }, |
382 | 2.37k | RPCExamples{ |
383 | 2.37k | HelpExampleCli("getindexinfo", "") |
384 | 2.37k | + HelpExampleRpc("getindexinfo", "") |
385 | 2.37k | + HelpExampleCli("getindexinfo", "txindex") |
386 | 2.37k | + HelpExampleRpc("getindexinfo", "txindex") |
387 | 2.37k | }, |
388 | 2.37k | [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue |
389 | 2.37k | { |
390 | 63 | UniValue result(UniValue::VOBJ); |
391 | 63 | const std::string index_name{self.MaybeArg<std::string_view>("index_name").value_or("")}; |
392 | | |
393 | 63 | if (g_txindex) { |
394 | 37 | result.pushKVs(SummaryToJSON(g_txindex->GetSummary(), index_name)); |
395 | 37 | } |
396 | | |
397 | 63 | if (g_coin_stats_index) { |
398 | 55 | result.pushKVs(SummaryToJSON(g_coin_stats_index->GetSummary(), index_name)); |
399 | 55 | } |
400 | | |
401 | 63 | if (g_txospenderindex) { |
402 | 29 | result.pushKVs(SummaryToJSON(g_txospenderindex->GetSummary(), index_name)); |
403 | 29 | } |
404 | | |
405 | 63 | ForEachBlockFilterIndex([&result, &index_name](const BlockFilterIndex& index) { |
406 | 44 | result.pushKVs(SummaryToJSON(index.GetSummary(), index_name)); |
407 | 44 | }); |
408 | | |
409 | 63 | return result; |
410 | 63 | }, |
411 | 2.37k | }; |
412 | 2.37k | } |
413 | | |
414 | | void RegisterNodeRPCCommands(CRPCTable& t) |
415 | 1.26k | { |
416 | 1.26k | static const CRPCCommand commands[]{ |
417 | 1.26k | {"control", &getmemoryinfo}, |
418 | 1.26k | {"control", &logging}, |
419 | 1.26k | {"util", &getindexinfo}, |
420 | 1.26k | {"hidden", &setmocktime}, |
421 | 1.26k | {"hidden", &mockscheduler}, |
422 | 1.26k | {"hidden", &echo}, |
423 | 1.26k | {"hidden", &echojson}, |
424 | 1.26k | {"hidden", &echoipc}, |
425 | 1.26k | }; |
426 | 10.1k | for (const auto& c : commands) { |
427 | 10.1k | t.appendCommand(c.name, &c); |
428 | 10.1k | } |
429 | 1.26k | } |