Skip to main content

veilid_core/veilid_api/
debug.rs

1////////////////////////////////////////////////////////////////
2// Debugging
3
4use super::*;
5use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
6use data_encoding::BASE64URL_NOPAD;
7use hashlink::LinkedHashMap;
8use network_manager::*;
9use routing_table::*;
10use std::fmt::Write;
11
12impl_veilid_log_facility!("veilid_debug");
13
14const DEBUG_HELP_TEMPLATE: &str = "{about-with-newline}{all-args}{after-help}";
15
16/// Debugging subcommands
17#[derive(Debug, Parser)]
18#[command(
19    name = "debug",
20    disable_help_flag = true,
21    disable_version_flag = true,
22    help_template = "{subcommands}"
23)]
24struct DebugCommandParser {
25    #[command(subcommand)]
26    command: DebugCommand,
27}
28
29/// Subcommands supported by top-level debug command text.
30#[derive(Debug, Subcommand)]
31#[command(help_template = DEBUG_HELP_TEMPLATE)]
32enum DebugCommand {
33    /// display this node's id values
34    Nodeid,
35    /// display routing table bucket statistics
36    Buckets {
37        /// Optional minimum state filter (`punished`, `dead`, `reliable`, `unreliable`).
38        #[arg(value_name = "min_state")]
39        min_state: Option<String>,
40    },
41    /// display dialinfo in this node's routing domains
42    Dialinfo,
43    /// display local peer info
44    Peerinfo {
45        /// Optional tokens in any order:
46        /// - routing domain: `public`, `local`, `pub`, `loc`
47        /// - publication mode: `published`, `current`
48        #[arg(value_name = "peerinfo_token")]
49        args: Vec<String>,
50    },
51    /// explain contact mechanism for a node
52    Contact {
53        /// Filtered node reference:
54        /// `<node>[+<sequencing>][/<protocoltype>][/<addresstype>][/<routingdomain>]`
55        /// where protocol is `udp|tcp|ws|wss`, address is `ipv4|ipv6`,
56        /// and routing domain is `pub|loc`.
57        #[arg(value_name = "node_ref")]
58        node_ref: String,
59    },
60    /// generate and display a random keypair
61    Keypair {
62        /// Optional crypto kind (`VLD0`, etc.).
63        #[arg(value_name = "cryptokind")]
64        cryptokind: Option<String>,
65    },
66    /// show routing table entry index
67    Entries {
68        /// Optional filters in any order:
69        /// - minimum state: `dead` or `reliable`
70        /// - mode: `fastest`
71        /// - capabilities list: comma-separated four-char codes
72        ///   (for example `ROUT,SGNL,RLAY,DIAL,DHTV,APPM`).
73        #[arg(value_name = "entries_token")]
74        args: Vec<String>,
75    },
76    /// show details for a routing table entry
77    Entry {
78        /// Node id or filtered node reference.
79        #[arg(value_name = "node")]
80        node: String,
81    },
82    /// list, clear, add, or remove address-filter punishments
83    Punish {
84        #[command(subcommand)]
85        command: Option<DebugPunishSubcommand>,
86    },
87    /// generate bootstrap TXT record data
88    Txtrecord {
89        /// Optional signing keypair group.
90        #[arg(value_name = "keypairs")]
91        keypairs: Option<String>,
92    },
93    /// change relays in use
94    Relay {
95        /// Relay configuration tokens:
96        /// `[<relay>...] [public|local]`
97        ///
98        /// `<relay>` accepts node id or filtered node forms.
99        #[arg(value_name = "relay_token")]
100        args: Vec<String>,
101    },
102    /// send a status RPC question to a destination
103    Ping {
104        /// Destination grammar:
105        /// - direct: `<node>[+<safety>][/<protocoltype>][/<addresstype>][/<routingdomain>]`
106        /// - relay: `<target>@<relay_dialinfo>`
107        /// - private: `#<index_or_route>[+<safety>]`
108        #[arg(value_name = "destination")]
109        destination: String,
110    },
111    /// send an app message statement
112    Appmessage {
113        /// Destination grammar matches `ping`.
114        #[arg(value_name = "destination")]
115        destination: String,
116        /// Data payload grammar:
117        /// - single-word string (for example `foobar`)
118        /// - shell-quoted string (for example `"foo\nbar\n"`)
119        /// - `#` followed by hex bytes (for example `#12AB34CD...`)
120        #[arg(value_name = "data")]
121        data: Vec<String>,
122    },
123    /// send an app call question
124    Appcall {
125        /// Destination grammar matches `ping`.
126        #[arg(value_name = "destination")]
127        destination: String,
128        /// Data payload grammar:
129        /// - single-word string (for example `foobar`)
130        /// - shell-quoted string (for example `"foo\nbar\n"`)
131        /// - `#` followed by hex bytes (for example `#12AB34CD...`)
132        #[arg(value_name = "data")]
133        data: Vec<String>,
134    },
135    /// reply to a pending app call
136    Appreply {
137        /// Optional `#call_id` value from a pending app call.
138        #[arg(value_name = "id")]
139        id: Option<String>,
140        /// Data payload grammar:
141        /// - single-word string (for example `foobar`)
142        /// - shell-quoted string (for example `"foo\nbar\n"`)
143        /// - `#` followed by hex bytes (for example `#12AB34CD...`)
144        #[arg(value_name = "data")]
145        data: Vec<String>,
146    },
147    /// resolve a destination across the network
148    Resolve {
149        /// Destination grammar matches `ping`.
150        #[arg(value_name = "destination")]
151        destination: String,
152    },
153    /// display detailed local node information
154    Nodeinfo,
155    /// purge local routing state
156    Purge {
157        #[command(subcommand)]
158        command: Option<DebugPurgeSubcommand>,
159    },
160    /// attach this node to the network
161    Attach,
162    /// detach this node from the network
163    Detach,
164    /// inspect or mutate config
165    Config {
166        /// First positional argument:
167        /// - `insecure` to read from full config view
168        /// - otherwise treated as a config key path.
169        #[arg(value_name = "mode_or_key")]
170        arg_0: Option<String>,
171        /// Optional config key path when `mode_or_key` is `insecure`.
172        ///
173        /// Config keys use dot-path grammar, for example:
174        /// `network.protocol.udp.enabled`.
175        #[arg(value_name = "config_key")]
176        arg_1: Option<String>,
177    },
178    /// restart network subsystem or inspect network stats
179    Network {
180        #[command(subcommand)]
181        command: Option<DebugNetworkSubcommand>,
182    },
183    /// allocate, publish, import, and test routes
184    Route {
185        #[command(subcommand)]
186        command: Option<DebugRouteSubcommand>,
187    },
188    /// list, open, inspect, and mutate DHT records
189    Record {
190        #[command(subcommand)]
191        command: Option<DebugRecordSubcommand>,
192    },
193    /// list tables and show table store details
194    Table {
195        #[command(subcommand)]
196        command: Option<DebugTableSubcommand>,
197    },
198    /// show process and attachment uptime
199    Uptime,
200    #[cfg(debug_assertions)]
201    /// trigger crash behaviors for debug testing
202    Die {
203        /// Crash mode (`panic`, `unwrap`, `unwrap_or_log`, `expect`, `expect_or_log`, `div0`, `overflow`, `oob`, `nullptr`, `unreachable`).
204        #[arg(value_name = "mode")]
205        mode: Option<String>,
206        /// Optional panic or expect message words.
207        #[arg(value_name = "message")]
208        message: Vec<String>,
209    },
210}
211
212/// Subcommands supported by `purge`.
213#[derive(Debug, Subcommand)]
214#[command(
215    help_template = DEBUG_HELP_TEMPLATE,
216    disable_help_subcommand = true
217)]
218enum DebugPurgeSubcommand {
219    /// Purge routing table buckets.
220    #[command(help_template = DEBUG_HELP_TEMPLATE)]
221    Buckets,
222    /// Purge recent peer connections.
223    #[command(help_template = DEBUG_HELP_TEMPLATE)]
224    Connections,
225    /// Purge route specifications.
226    #[command(help_template = DEBUG_HELP_TEMPLATE)]
227    Routes,
228}
229
230/// Subcommands supported by `network`.
231#[derive(Debug, Subcommand)]
232#[command(
233    help_template = DEBUG_HELP_TEMPLATE,
234    disable_help_subcommand = true
235)]
236enum DebugNetworkSubcommand {
237    /// Restart the low-level network subsystem.
238    #[command(help_template = DEBUG_HELP_TEMPLATE)]
239    Restart,
240    /// Print network manager statistics.
241    #[command(help_template = DEBUG_HELP_TEMPLATE)]
242    Stats,
243}
244
245/// Subcommands supported by `route`.
246#[derive(Debug, Subcommand)]
247#[command(
248    help_template = DEBUG_HELP_TEMPLATE,
249    disable_help_subcommand = true
250)]
251enum DebugRouteSubcommand {
252    /// Allocate a new route.
253    #[command(help_template = DEBUG_HELP_TEMPLATE)]
254    Allocate {
255        /// Optional allocation arguments in any order:
256        /// - sequencing: `ord`, `ord!`, `uno`
257        /// - stability: `rel` or `low`
258        /// - hop count: positive integer
259        /// - direction: `in` or `out`
260        #[arg(allow_hyphen_values = true, value_name = "allocate_arg")]
261        params: Vec<String>,
262    },
263    /// Release an allocated route.
264    #[command(help_template = DEBUG_HELP_TEMPLATE)]
265    Release {
266        /// Route id (full or prefix).
267        #[arg(value_name = "route_id")]
268        route_id: String,
269    },
270    /// Publish a route blob for import on another node.
271    #[command(help_template = DEBUG_HELP_TEMPLATE)]
272    Publish {
273        /// Route id (full or prefix).
274        #[arg(value_name = "route_id")]
275        route_id: String,
276        /// Include full route details when set to `full`.
277        #[arg(value_name = "full")]
278        full: Option<String>,
279    },
280    /// Mark a route as no longer published.
281    #[command(help_template = DEBUG_HELP_TEMPLATE)]
282    Unpublish {
283        /// Route id (full or prefix).
284        #[arg(value_name = "route_id")]
285        route_id: String,
286    },
287    /// Show details for a route id or route key.
288    #[command(help_template = DEBUG_HELP_TEMPLATE)]
289    Print {
290        /// Route id or route key (full or prefix).
291        #[arg(value_name = "route")]
292        route: String,
293    },
294    /// List allocated and imported routes.
295    #[command(help_template = DEBUG_HELP_TEMPLATE)]
296    List,
297    /// Import a published route blob.
298    #[command(help_template = DEBUG_HELP_TEMPLATE)]
299    Import {
300        /// Base64URL route blob from `route publish`.
301        #[arg(value_name = "blob")]
302        blob: String,
303    },
304    /// Test an allocated or imported route.
305    #[command(help_template = DEBUG_HELP_TEMPLATE)]
306    Test {
307        /// Route id (full or prefix).
308        #[arg(value_name = "route_id")]
309        route_id: String,
310    },
311}
312
313/// Subcommands supported by `record`.
314#[derive(Debug, Subcommand)]
315#[command(
316    help_template = DEBUG_HELP_TEMPLATE,
317    disable_help_subcommand = true
318)]
319enum DebugRecordSubcommand {
320    /// List records by scope.
321    #[command(help_template = DEBUG_HELP_TEMPLATE)]
322    List {
323        /// Record scope (`local`, `remote`, `opened`, `offline`, `watched`, `transactions`).
324        #[arg(value_name = "scope")]
325        scope: String,
326    },
327    /// Purge local or remote records.
328    #[command(help_template = DEBUG_HELP_TEMPLATE)]
329    Purge {
330        /// Scope to purge (`local` or `remote`).
331        #[arg(value_name = "scope")]
332        scope: String,
333        /// Optional byte target to purge down to. Conflicts with `--keys`.
334        #[arg(value_name = "bytes", group = "target")]
335        bytes: Option<u64>,
336        /// Optional comma-delimited list of record keys to purge. No spaces allowed, conflicts with `bytes`.
337        #[arg(long, value_name = "keys", value_delimiter = ',', group = "target")]
338        keys: Option<Vec<String>>,
339    },
340    /// Create a new DHT record.
341    #[command(help_template = DEBUG_HELP_TEMPLATE)]
342    Create {
343        /// DHT schema JSON or default subkey count.
344        #[arg(value_name = "dht_schema")]
345        dht_schema: Option<String>,
346        /// Crypto kind (for example `VLD0`).
347        #[arg(value_name = "crypto_kind")]
348        crypto_kind: Option<String>,
349        /// Safety selection expression.
350        #[arg(value_name = "safety")]
351        safety: Option<String>,
352    },
353    /// Open an existing DHT record.
354    #[command(help_template = DEBUG_HELP_TEMPLATE)]
355    Open {
356        /// Record key with optional `+safety` suffix.
357        #[arg(value_name = "key")]
358        key: String,
359        /// Optional writer keypair.
360        #[arg(value_name = "writer")]
361        writer: Option<String>,
362    },
363    /// Close an opened DHT record.
364    #[command(help_template = DEBUG_HELP_TEMPLATE)]
365    Close {
366        /// Optional key (defaults to most recently opened record).
367        #[arg(value_name = "key")]
368        key: Option<String>,
369    },
370    /// Read a value from a DHT record subkey.
371    #[command(help_template = DEBUG_HELP_TEMPLATE)]
372    Get {
373        /// Positional form:
374        /// - `[key] <subkey> [force]`
375        /// - if `key` is omitted, the most recently opened record is used
376        /// - `force` refreshes from the network
377        #[arg(allow_hyphen_values = true, value_name = "get_arg")]
378        params: Vec<String>,
379    },
380    /// Write a value to a DHT record subkey.
381    #[command(help_template = DEBUG_HELP_TEMPLATE)]
382    Set {
383        /// Positional form:
384        /// - `[key] <subkey> <data> [writer] [offline|online|true|false]`
385        /// - if `key` is omitted, the most recently opened record is used
386        /// - `<data>` may be plain text, quoted text, or `#`-prefixed hex
387        #[arg(allow_hyphen_values = true, value_name = "set_arg")]
388        params: Vec<String>,
389    },
390    /// Delete the local copy of a DHT record.
391    #[command(help_template = DEBUG_HELP_TEMPLATE)]
392    Delete {
393        /// Record key.
394        #[arg(value_name = "key")]
395        key: String,
396    },
397    /// Show local and remote record information.
398    #[command(help_template = DEBUG_HELP_TEMPLATE)]
399    Info {
400        /// Record key.
401        #[arg(value_name = "key")]
402        key: String,
403        /// Optional subkey to inspect.
404        #[arg(value_name = "subkey")]
405        subkey: Option<u32>,
406    },
407    /// Watch a record for value changes.
408    #[command(help_template = DEBUG_HELP_TEMPLATE)]
409    Watch {
410        /// Optional key (defaults to most recently opened record).
411        #[arg(value_name = "key")]
412        key: Option<String>,
413        /// Optional subkeys or ranges.
414        #[arg(value_name = "subkeys")]
415        subkeys: Option<String>,
416        /// Optional expiration duration.
417        #[arg(value_name = "expiration")]
418        expiration: Option<String>,
419        /// Optional max change count.
420        #[arg(value_name = "count")]
421        count: Option<u32>,
422    },
423    /// Cancel a record watch.
424    #[command(help_template = DEBUG_HELP_TEMPLATE)]
425    Cancel {
426        /// Optional key (defaults to most recently opened record).
427        #[arg(value_name = "key")]
428        key: Option<String>,
429        /// Optional subkeys or ranges.
430        #[arg(value_name = "subkeys")]
431        subkeys: Option<String>,
432    },
433    /// Inspect DHT record subkey status.
434    #[command(help_template = DEBUG_HELP_TEMPLATE)]
435    Inspect {
436        /// Optional key (defaults to most recently opened record).
437        #[arg(value_name = "key")]
438        key: Option<String>,
439        /// Optional inspect scope (`local|syncget|syncset|updateget|updateset`).
440        #[arg(value_name = "scope")]
441        scope: Option<String>,
442        /// Optional subkeys or ranges.
443        #[arg(value_name = "subkeys")]
444        subkeys: Option<String>,
445    },
446    /// Rehydrate expired local record data onto the network.
447    #[command(help_template = DEBUG_HELP_TEMPLATE)]
448    Rehydrate {
449        /// Record key.
450        #[arg(value_name = "key")]
451        key: String,
452        /// Optional subkeys or ranges.
453        #[arg(value_name = "subkeys")]
454        subkeys: Option<String>,
455        /// Optional consensus count.
456        #[arg(value_name = "consensus_count")]
457        consensus_count: Option<usize>,
458    },
459}
460
461/// Subcommands supported by `table`.
462#[derive(Debug, Subcommand)]
463#[command(
464    help_template = DEBUG_HELP_TEMPLATE,
465    disable_help_subcommand = true
466)]
467enum DebugTableSubcommand {
468    /// List all table store tables.
469    #[command(help_template = DEBUG_HELP_TEMPLATE)]
470    List,
471    /// Show details and IO stats for a table.
472    #[command(help_template = DEBUG_HELP_TEMPLATE)]
473    Info {
474        /// Table name.
475        #[arg(value_name = "name")]
476        name: String,
477    },
478}
479
480/// Subcommands supported by `punish`.
481#[derive(Debug, Subcommand)]
482#[command(
483    help_template = DEBUG_HELP_TEMPLATE,
484    disable_help_subcommand = true
485)]
486enum DebugPunishSubcommand {
487    /// List all address-filter punishments.
488    #[command(help_template = DEBUG_HELP_TEMPLATE)]
489    List,
490    /// Clear all address-filter punishments.
491    #[command(help_template = DEBUG_HELP_TEMPLATE)]
492    Clear,
493    /// Add a punishment for a node id or IP address.
494    #[command(help_template = DEBUG_HELP_TEMPLATE)]
495    Add {
496        /// Target node id (`VLD0:...`) or IP address.
497        #[arg(value_name = "target")]
498        target: String,
499    },
500    /// Remove a punishment for a node id or IP address.
501    #[command(help_template = DEBUG_HELP_TEMPLATE)]
502    Remove {
503        /// Target node id (`VLD0:...`) or IP address.
504        #[arg(value_name = "target")]
505        target: String,
506    },
507}
508
509fn render_parser_help<T: CommandFactory>() -> String {
510    let mut command = T::command();
511    let mut bytes = Vec::new();
512    if command.write_long_help(&mut bytes).is_err() {
513        return String::new();
514    }
515    String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(&e.into_bytes()).into())
516}
517
518fn join_args(args: Vec<String>) -> String {
519    args.join(" ")
520}
521
522#[derive(Default)]
523pub(crate) struct DebugCache {
524    pub imported_routes: Vec<RouteId>,
525    pub opened_record_contexts: LinkedHashMap<RecordKey, RoutingContext>,
526}
527
528#[must_use]
529pub(crate) fn format_opt_ts(ts: Option<TimestampDuration>) -> String {
530    let Some(ts) = ts else {
531        return "---".to_owned();
532    };
533    format!("{:#}", ts)
534}
535
536#[must_use]
537pub(crate) fn format_opt_bps(bps: Option<ByteCount>) -> String {
538    let Some(bps) = bps else {
539        return "---".to_owned();
540    };
541    format!("{:#}/s", bps)
542}
543
544fn parse_bucket_entry_state(text: &str) -> Result<BucketEntryState, String> {
545    match text {
546        "punished" => Ok(BucketEntryState::Punished),
547        "dead" => Ok(BucketEntryState::Dead),
548        "reliable" => Ok(BucketEntryState::Reliable),
549        "unreliable" => Ok(BucketEntryState::Unreliable),
550        _ => Err(format!("invalid bucket entry state: {text}")),
551    }
552}
553
554fn get_bucket_entry_state(text: &str) -> Option<BucketEntryState> {
555    parse_bucket_entry_state(text).ok()
556}
557
558fn get_string(text: &str) -> Option<String> {
559    Some(text.to_owned())
560}
561
562fn parse_data(text: &str) -> Result<Vec<u8>, String> {
563    if let Some(stripped_text) = text.strip_prefix('#') {
564        hex::decode(stripped_text).map_err(|e| format!("invalid hex data: {e}"))
565    } else if text.starts_with('"') || text.starts_with('\'') {
566        serde_json::from_str::<String>(text)
567            .map(|x| x.into_bytes())
568            .map_err(|e| format!("invalid quoted data: {e}"))
569    } else {
570        Ok(text.as_bytes().to_vec())
571    }
572}
573
574fn get_data(text: &str) -> Option<Vec<u8>> {
575    parse_data(text).ok()
576}
577
578fn parse_subkeys(text: &str) -> Result<ValueSubkeyRangeSet, String> {
579    if let Some(n) = get_number::<u32>(text) {
580        Ok(ValueSubkeyRangeSet::single(n))
581    } else {
582        ValueSubkeyRangeSet::from_str(text).map_err(|e| format!("invalid subkeys: {e}"))
583    }
584}
585
586fn get_subkeys(text: &str) -> Option<ValueSubkeyRangeSet> {
587    parse_subkeys(text).ok()
588}
589
590fn parse_dht_report_scope(text: &str) -> Result<DHTReportScope, String> {
591    match text.to_ascii_lowercase().trim() {
592        "local" => Ok(DHTReportScope::Local),
593        "syncget" => Ok(DHTReportScope::SyncGet),
594        "syncset" => Ok(DHTReportScope::SyncSet),
595        "updateget" => Ok(DHTReportScope::UpdateGet),
596        "updateset" => Ok(DHTReportScope::UpdateSet),
597        _ => Err(format!("invalid dht report scope: {text}")),
598    }
599}
600
601fn get_dht_report_scope(text: &str) -> Option<DHTReportScope> {
602    parse_dht_report_scope(text).ok()
603}
604
605enum PublishedState {
606    Published,
607    Current,
608}
609
610impl PublishedState {
611    fn as_bool(&self) -> bool {
612        matches!(self, Self::Published)
613    }
614}
615
616impl FromStr for PublishedState {
617    type Err = String;
618
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        match s.to_ascii_lowercase().as_str() {
621            "published" => Ok(Self::Published),
622            "current" => Ok(Self::Current),
623            _ => Err(format!("invalid published mode: {s}")),
624        }
625    }
626}
627
628fn get_nested_debug_family_name(arg: Option<&str>) -> Option<String> {
629    let top_level = arg?;
630    let parser = DebugCommandParser::command();
631    let command = parser.find_subcommand(top_level)?;
632    if command.get_subcommands().next().is_some() {
633        Some(top_level.to_owned())
634    } else {
635        None
636    }
637}
638
639fn get_known_debug_command_name(arg: Option<&str>) -> Option<String> {
640    let top_level = arg?;
641    let parser = DebugCommandParser::command();
642    if parser.find_subcommand(top_level).is_some() {
643        Some(top_level.to_owned())
644    } else {
645        None
646    }
647}
648
649fn get_route_id(
650    registry: VeilidComponentRegistry,
651    allow_allocated: bool,
652    allow_remote: bool,
653) -> impl Fn(&str) -> Option<RouteId> {
654    move |text: &str| {
655        if text.is_empty() {
656            return None;
657        }
658        let routing_table = registry.routing_table();
659        let rss = routing_table.route_spec_store();
660
661        match RouteId::from_str(text).ok() {
662            Some(key) => {
663                if allow_allocated {
664                    let routes: Vec<RouteId> =
665                        rss.list_allocated_routes(|k, _| Some(k.clone().into()));
666                    if routes.contains(&key) {
667                        return Some(key);
668                    }
669                }
670                if allow_remote {
671                    let rroutes: Vec<RouteId> =
672                        rss.list_remote_routes(|k, _| Some(k.clone().into()));
673                    if rroutes.contains(&key) {
674                        return Some(key);
675                    }
676                }
677            }
678            None => {
679                if allow_allocated {
680                    let routes: Vec<RouteId> =
681                        rss.list_allocated_routes(|k, _| Some(k.clone().into()));
682                    for r in routes {
683                        let rkey = r.to_string();
684                        if rkey.starts_with(text)
685                            || rkey
686                                .split_once(':')
687                                .map(|(_, b)| b.starts_with(text))
688                                .unwrap_or(false)
689                        {
690                            return Some(r);
691                        }
692                    }
693                }
694                if allow_remote {
695                    let routes: Vec<RouteId> =
696                        rss.list_remote_routes(|k, _| Some(k.clone().into()));
697                    for r in routes {
698                        let rkey = r.to_string();
699                        if rkey.starts_with(text)
700                            || rkey
701                                .split_once(':')
702                                .map(|(_, b)| b.starts_with(text))
703                                .unwrap_or(false)
704                        {
705                            return Some(r);
706                        }
707                    }
708                }
709            }
710        }
711        None
712    }
713}
714
715fn get_route_key(
716    registry: VeilidComponentRegistry,
717    allow_allocated: bool,
718    allow_remote: bool,
719) -> impl Fn(&str) -> Option<PublicKey> {
720    move |text: &str| {
721        if text.is_empty() {
722            return None;
723        }
724        let routing_table = registry.routing_table();
725        let rss = routing_table.route_spec_store();
726
727        match PublicKey::from_str(text).ok() {
728            Some(key) => {
729                if allow_allocated {
730                    let allocated_route_keys =
731                        rss.list_allocated_routes(|_, arce| Some(arce.route_set_keys().to_vec()));
732                    if let Some(found_key) = allocated_route_keys
733                        .into_iter()
734                        .flatten()
735                        .find(|x| x == &key)
736                    {
737                        return Some(found_key);
738                    }
739                }
740                if allow_remote {
741                    let remote_route_keys = rss.list_remote_routes(|_, v| {
742                        Some(
743                            v.get_private_routes()
744                                .iter()
745                                .map(|x| x.public_key.clone())
746                                .collect::<Vec<_>>(),
747                        )
748                    });
749                    if let Some(found_key) =
750                        remote_route_keys.into_iter().flatten().find(|x| x == &key)
751                    {
752                        return Some(found_key);
753                    }
754                }
755            }
756            None => {
757                if allow_allocated {
758                    let allocated_route_keys =
759                        rss.list_allocated_routes(|_, arce| Some(arce.route_set_keys().to_vec()));
760                    for r in allocated_route_keys.into_iter().flatten() {
761                        let rkey = r.to_string();
762                        if rkey.starts_with(text)
763                            || rkey
764                                .split_once(':')
765                                .map(|(_, b)| b.starts_with(text))
766                                .unwrap_or(false)
767                        {
768                            return Some(r);
769                        }
770                    }
771                }
772                if allow_remote {
773                    let remote_route_keys = rss.list_remote_routes(|_, v| {
774                        Some(
775                            v.get_private_routes()
776                                .iter()
777                                .map(|x| x.public_key.clone())
778                                .collect::<Vec<_>>(),
779                        )
780                    });
781                    for r in remote_route_keys.into_iter().flatten() {
782                        let rkey = r.to_string();
783                        if rkey.starts_with(text)
784                            || rkey
785                                .split_once(':')
786                                .map(|(_, b)| b.starts_with(text))
787                                .unwrap_or(false)
788                        {
789                            return Some(r);
790                        }
791                    }
792                }
793            }
794        }
795        None
796    }
797}
798
799fn get_dht_schema(text: &str) -> Option<VeilidAPIResult<DHTSchema>> {
800    if text.is_empty() {
801        return None;
802    }
803    if let Ok(n) = u16::from_str(text) {
804        return Some(DHTSchema::dflt(n));
805    }
806    Some(deserialize_json::<DHTSchema>(text))
807}
808
809fn get_safety_selection(
810    registry: VeilidComponentRegistry,
811) -> impl Fn(&str) -> Option<SafetySelection> {
812    move |text| {
813        let default_route_hop_count =
814            registry.config().network.rpc.default_route_hop_count as usize;
815
816        if !text.is_empty() && &text[0..1] == "-" {
817            // Unsafe
818            let text = &text[1..];
819            let seq = get_sequencing(text).unwrap_or_default();
820            Some(SafetySelection::Unsafe(seq))
821        } else {
822            // Safe
823            let mut preferred_route = None;
824            let mut hop_count = default_route_hop_count;
825            let mut stability = Stability::Reliable;
826            let mut sequencing = Sequencing::PreferUnordered;
827            for x in text.split(',') {
828                let x = x.trim();
829                if let Some(pr) = get_route_id(registry.clone(), true, false)(x) {
830                    preferred_route = Some(pr)
831                }
832                if let Some(n) = get_number(x) {
833                    hop_count = n;
834                }
835                if let Some(s) = get_stability(x) {
836                    stability = s;
837                }
838                if let Some(s) = get_sequencing(x) {
839                    sequencing = s;
840                }
841            }
842
843            let ss = SafetySpec {
844                preferred_route,
845                hop_count,
846                stability,
847                sequencing,
848            };
849            Some(SafetySelection::Safe(ss))
850        }
851    }
852}
853
854fn get_node_ref_modifiers(node_ref: NodeRef) -> impl FnOnce(&str) -> Option<FilteredNodeRef> {
855    move |text| {
856        let mut node_ref = node_ref.sequencing_filtered(Sequencing::PreferUnordered);
857        for m in text.split('/') {
858            if let Some(pt) = get_protocol_type(m) {
859                node_ref.merge_filter(NodeRefFilter::new().with_protocol_type(pt));
860            } else if let Some(at) = get_address_type(m) {
861                node_ref.merge_filter(NodeRefFilter::new().with_address_type(at));
862            } else if let Some(rd) = get_routing_domain(m) {
863                node_ref.merge_filter(NodeRefFilter::new().with_routing_domain(rd));
864            } else {
865                return None;
866            }
867        }
868        Some(node_ref)
869    }
870}
871
872fn get_number<T: num_traits::Num + FromStr>(text: &str) -> Option<T> {
873    T::from_str(text).ok()
874}
875
876fn get_record_key(text: &str) -> Option<RecordKey> {
877    RecordKey::from_str(text).ok()
878}
879fn get_bare_node_id(text: &str) -> Option<BareNodeId> {
880    let bare_node_id = BareNodeId::from_str(text).ok()?;
881
882    // Enforce 32 byte node ids
883    if bare_node_id.len() != 32 {
884        return None;
885    }
886    Some(bare_node_id)
887}
888fn get_node_id(text: &str) -> Option<NodeId> {
889    let node_id = NodeId::from_str(text).ok()?;
890
891    // Enforce 32 byte node ids
892    if node_id.value().len() != 32 {
893        return None;
894    }
895    Some(node_id)
896}
897fn get_keypair(text: &str) -> Option<KeyPair> {
898    KeyPair::from_str(text).ok()
899}
900fn get_keypair_group(text: &str) -> Option<KeyPairGroup> {
901    KeyPairGroup::from_str(text).ok()
902}
903
904fn get_crypto_system_version<'a>(
905    crypto: &'a Crypto,
906) -> impl FnOnce(&str) -> Option<CryptoSystemGuard<'a>> {
907    move |text| {
908        let kindstr = get_string(text)?;
909        let kind = CryptoKind::from_str(&kindstr).ok()?;
910        crypto.get(kind)
911    }
912}
913
914fn get_dht_key_no_safety(text: &str) -> Option<RecordKey> {
915    let key = get_record_key(text)?;
916
917    Some(key)
918}
919
920fn get_dht_key(
921    registry: VeilidComponentRegistry,
922) -> impl FnOnce(&str) -> Option<(RecordKey, Option<SafetySelection>)> {
923    move |text| {
924        // Safety selection
925        let (text, ss) = if let Some((first, second)) = text.split_once('+') {
926            let ss = get_safety_selection(registry)(second)?;
927            (first, Some(ss))
928        } else {
929            (text, None)
930        };
931        if text.is_empty() {
932            return None;
933        }
934
935        let key = get_record_key(text)?;
936
937        Some((key, ss))
938    }
939}
940
941fn resolve_node_ref(
942    registry: VeilidComponentRegistry,
943    safety_selection: SafetySelection,
944) -> impl FnOnce(&str) -> PinBoxFutureStatic<Option<NodeRef>> {
945    move |text| {
946        let text = text.to_owned();
947        Box::pin(async move {
948            let nr = if let Some(node_id) = get_node_id(&text) {
949                registry
950                    .rpc_processor()
951                    .resolve_node(node_id, safety_selection)
952                    .await
953                    .ok()
954                    .flatten()?
955            } else {
956                return None;
957            };
958            Some(nr)
959        })
960    }
961}
962
963fn resolve_filtered_node_ref(
964    registry: VeilidComponentRegistry,
965    safety_selection: SafetySelection,
966) -> impl FnOnce(&str) -> PinBoxFutureStatic<Option<FilteredNodeRef>> {
967    move |text| {
968        let text = text.to_owned();
969        Box::pin(async move {
970            let (text, mods) = text
971                .split_once('/')
972                .map(|x| (x.0, Some(x.1)))
973                .unwrap_or((&text, None));
974
975            let nr = if let Some(node_id) = get_node_id(text) {
976                registry
977                    .rpc_processor()
978                    .resolve_node(node_id, safety_selection)
979                    .await
980                    .ok()
981                    .flatten()?
982            } else {
983                return None;
984            };
985            if let Some(mods) = mods {
986                Some(get_node_ref_modifiers(nr)(mods)?)
987            } else {
988                Some(nr.sequencing_filtered(Sequencing::PreferUnordered))
989            }
990        })
991    }
992}
993
994fn get_node_ref(registry: VeilidComponentRegistry) -> impl FnOnce(&str) -> Option<NodeRef> {
995    move |text| {
996        let routing_table = registry.routing_table();
997        let nr = if let Some(key) = get_bare_node_id(text) {
998            routing_table.lookup_bare_node_id(key).ok().flatten()?
999        } else if let Some(node_id) = get_node_id(text) {
1000            routing_table.lookup_node_id(node_id).ok().flatten()?
1001        } else {
1002            return None;
1003        };
1004        Some(nr)
1005    }
1006}
1007
1008fn get_filtered_node_ref(
1009    registry: VeilidComponentRegistry,
1010) -> impl FnOnce(&str) -> Option<FilteredNodeRef> {
1011    move |text| {
1012        let routing_table = registry.routing_table();
1013        FilteredNodeRef::parse(&routing_table, text).ok().flatten()
1014    }
1015}
1016
1017fn get_protocol_type(text: &str) -> Option<ProtocolType> {
1018    ProtocolType::from_str(text).ok()
1019}
1020fn get_sequencing(text: &str) -> Option<Sequencing> {
1021    Sequencing::from_str(text).ok()
1022}
1023fn get_stability(text: &str) -> Option<Stability> {
1024    Stability::from_str(text).ok()
1025}
1026fn get_direction_set(text: &str) -> Option<DirectionSet> {
1027    Direction::set_from_str(text).ok()
1028}
1029
1030fn get_address_type(text: &str) -> Option<AddressType> {
1031    AddressType::from_str(text).ok()
1032}
1033fn get_routing_domain(text: &str) -> Option<RoutingDomain> {
1034    RoutingDomain::from_str(text).ok()
1035}
1036
1037fn get_ip_addr(text: &str) -> Option<IpAddr> {
1038    IpAddr::from_str(text).ok()
1039}
1040
1041fn get_published(text: &str) -> Option<bool> {
1042    PublishedState::from_str(text).ok().map(|x| x.as_bool())
1043}
1044
1045fn get_debug_argument<T, G: FnOnce(&str) -> Option<T>>(
1046    value: &str,
1047    context: &str,
1048    argument: &str,
1049    getter: G,
1050) -> VeilidAPIResult<T> {
1051    let Some(val) = getter(value) else {
1052        apibail_invalid_argument!(context, argument, value);
1053    };
1054    Ok(val)
1055}
1056
1057async fn async_get_debug_argument<T, G: FnOnce(&str) -> PinBoxFutureStatic<Option<T>>>(
1058    value: &str,
1059    context: &str,
1060    argument: &str,
1061    getter: G,
1062) -> VeilidAPIResult<T> {
1063    let Some(val) = getter(value).await else {
1064        apibail_invalid_argument!(context, argument, value);
1065    };
1066    Ok(val)
1067}
1068
1069fn get_debug_argument_at<T, G: FnOnce(&str) -> Option<T>>(
1070    debug_args: &[String],
1071    pos: usize,
1072    context: &str,
1073    argument: &str,
1074    getter: G,
1075) -> VeilidAPIResult<T> {
1076    if pos >= debug_args.len() {
1077        apibail_missing_argument!(context, argument);
1078    }
1079    let value = &debug_args[pos];
1080    let Some(val) = getter(value) else {
1081        apibail_invalid_argument!(context, argument, value.clone());
1082    };
1083    Ok(val)
1084}
1085
1086async fn async_get_debug_argument_at<T, G: FnOnce(&str) -> PinBoxFutureStatic<Option<T>>>(
1087    debug_args: &[String],
1088    pos: usize,
1089    context: &str,
1090    argument: &str,
1091    getter: G,
1092) -> VeilidAPIResult<T> {
1093    if pos >= debug_args.len() {
1094        apibail_missing_argument!(context, argument);
1095    }
1096    let value = &debug_args[pos];
1097    let Some(val) = getter(value).await else {
1098        apibail_invalid_argument!(context, argument, value.clone());
1099    };
1100    Ok(val)
1101}
1102
1103impl VeilidAPI {
1104    fn debug_buckets(&self, opt_min_state: Option<String>) -> VeilidAPIResult<String> {
1105        let mut min_state = BucketEntryState::Unreliable;
1106        if let Some(min_state_str) = opt_min_state {
1107            min_state = get_debug_argument(
1108                &min_state_str,
1109                "debug_buckets",
1110                "min_state",
1111                get_bucket_entry_state,
1112            )?;
1113        }
1114        // Dump routing table bucket info
1115        let routing_table = self.core_context()?.routing_table();
1116        Ok(routing_table.debug_info_buckets(min_state))
1117    }
1118
1119    fn debug_dialinfo(&self) -> VeilidAPIResult<String> {
1120        // Dump routing table dialinfo
1121        let routing_table = self.core_context()?.routing_table();
1122        Ok(routing_table.debug_info_dialinfo())
1123    }
1124    fn debug_peerinfo(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1125        // Dump routing table peerinfo
1126        let routing_table = self.core_context()?.routing_table();
1127
1128        let mut ai = 0;
1129        let mut opt_routing_domain = None;
1130        let mut opt_published = None;
1131
1132        while ai < args.len() {
1133            if let Ok(routing_domain) = get_debug_argument_at(
1134                &args,
1135                ai,
1136                "debug_peerinfo",
1137                "routing_domain",
1138                get_routing_domain,
1139            ) {
1140                opt_routing_domain = Some(routing_domain);
1141            } else if let Ok(published) =
1142                get_debug_argument_at(&args, ai, "debug_peerinfo", "published", get_published)
1143            {
1144                opt_published = Some(published);
1145            }
1146            ai += 1;
1147        }
1148
1149        let routing_domain = opt_routing_domain.unwrap_or(RoutingDomain::PublicInternet);
1150        let published = opt_published.unwrap_or(true);
1151
1152        Ok(routing_table.debug_info_peerinfo(routing_domain, published))
1153    }
1154
1155    async fn debug_txtrecord(&self, keypairs: Option<String>) -> VeilidAPIResult<String> {
1156        // Dump routing table txt record
1157        let signing_key_pairs = if let Some(keypairs) = keypairs {
1158            get_debug_argument(
1159                &keypairs,
1160                "debug_txtrecord",
1161                "signing_key_pairs",
1162                get_keypair_group,
1163            )?
1164        } else {
1165            KeyPairGroup::new()
1166        };
1167
1168        let network_manager = self.core_context()?.network_manager();
1169        Ok(network_manager
1170            .debug_info_txtrecord(signing_key_pairs)
1171            .await)
1172    }
1173
1174    fn debug_keypair(&self, cryptokind: Option<String>) -> VeilidAPIResult<String> {
1175        let crypto = self.crypto()?;
1176
1177        let vcrypto = if let Some(cryptokind) = cryptokind {
1178            get_debug_argument(
1179                &cryptokind,
1180                "debug_keypair",
1181                "kind",
1182                get_crypto_system_version(&crypto),
1183            )?
1184        } else {
1185            crypto.best()
1186        };
1187
1188        // Generate a keypair
1189        let out = vcrypto.generate_keypair().to_string();
1190        Ok(out)
1191    }
1192
1193    fn debug_entries(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1194        let mut min_state = BucketEntryState::Missing;
1195        let mut capabilities = vec![];
1196        let mut fastest = false;
1197        for arg in args {
1198            if let Some(ms) = get_bucket_entry_state(&arg) {
1199                min_state = ms;
1200            } else if arg == "fastest" {
1201                fastest = true;
1202            } else {
1203                for cap in arg.split(',') {
1204                    if let Ok(cap) = VeilidCapability::from_str(cap) {
1205                        capabilities.push(cap);
1206                    } else {
1207                        apibail_invalid_argument!("debug_entries", "unknown", arg);
1208                    }
1209                }
1210            }
1211        }
1212
1213        // Dump routing table entries
1214        let routing_table = self.core_context()?.routing_table();
1215        Ok(match fastest {
1216            true => routing_table.debug_info_entries_fastest(min_state, capabilities, 100000),
1217            false => routing_table.debug_info_entries(min_state, capabilities),
1218        })
1219    }
1220
1221    fn debug_entry(&self, node: String) -> VeilidAPIResult<String> {
1222        let registry = self.core_context()?.registry();
1223
1224        let node_ref = get_debug_argument(
1225            &node,
1226            "debug_entry",
1227            "node_id",
1228            get_node_ref(registry.clone()),
1229        )?;
1230
1231        // Dump routing table entry
1232        Ok(registry.routing_table().debug_info_entry(node_ref))
1233    }
1234
1235    async fn debug_relay(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1236        let registry = self.core_context()?.registry();
1237
1238        let mut relay_nodes_refs: Vec<NodeRef> = vec![];
1239        let mut routing_domain = RoutingDomain::PublicInternet;
1240        for n in 0..args.len() {
1241            let opt_relay_node = async_get_debug_argument_at(
1242                &args,
1243                n,
1244                "debug_relay",
1245                "node_id",
1246                resolve_node_ref(
1247                    registry.clone(),
1248                    SafetySelection::Unsafe(Sequencing::PreferUnordered),
1249                ),
1250            )
1251            .await
1252            .ok();
1253            if let Some(relay_node) = opt_relay_node {
1254                relay_nodes_refs.push(relay_node);
1255                continue;
1256            }
1257
1258            let opt_routing_domain = get_debug_argument_at(
1259                &args,
1260                n,
1261                "debug_relay",
1262                "routing_domain",
1263                get_routing_domain,
1264            )
1265            .ok();
1266            if let Some(rd) = opt_routing_domain {
1267                routing_domain = rd;
1268                break;
1269            }
1270        }
1271
1272        let relays: Vec<_> = relay_nodes_refs
1273            .iter()
1274            .cloned()
1275            .map(|nr| RoutingDomainRelay::new(routing_domain, nr, RelayKind::Inbound))
1276            .collect();
1277
1278        // Change routing domain relays
1279        let routing_table = registry.routing_table();
1280        {
1281            let rdc = routing_table.get_routing_domain_controller(routing_domain);
1282
1283            // Create a relay compilation with these relays
1284            let opt_relay_compilation = if relays.is_empty() {
1285                None
1286            } else {
1287                let rdd = rdc.read_dyn();
1288                let relay_requirements = rdd.relay_requirements();
1289                let mut rc = relay_requirements.make_relay_compiler();
1290                for relay in relays {
1291                    rc.apply_relay(relay);
1292                }
1293
1294                let c = rc.compile();
1295                if c.is_none() {
1296                    return Ok("Relays selected do not meet publication requirements".to_owned());
1297                }
1298                c
1299            };
1300
1301            let mut editor = rdc.edit_dyn();
1302            editor.set_relay_compilation(opt_relay_compilation);
1303            editor.commit();
1304            rdc.publish_peer_info();
1305        }
1306
1307        Ok("Relay changed".to_owned())
1308    }
1309
1310    async fn debug_nodeinfo(&self) -> VeilidAPIResult<String> {
1311        // Dump routing table entry
1312        let registry = self.core_context()?.registry();
1313        let nodeinfo_rtab = registry.routing_table().debug_info_nodeinfo();
1314        let nodeinfo_net = registry.network_manager().debug_info_nodeinfo();
1315        let nodeinfo_rpc = registry.rpc_processor().debug_info_nodeinfo();
1316        let nodeinfo_crypto = registry.crypto().debug_info_nodeinfo();
1317        let nodeinfo_attach = registry.attachment_manager().debug_info_nodeinfo();
1318
1319        // Dump core state
1320        let state = self.get_state().await?;
1321
1322        let mut peertable = Vec::new();
1323        peertable.push(format!(
1324            "Recent Peers: {} (max {})",
1325            state.network.peers.len(),
1326            RECENT_PEERS_TABLE_SIZE
1327        ));
1328        for peer in state.network.peers {
1329            peertable.push(format!(
1330                "   {} | {} | {} | {} down | {} up",
1331                peer.node_ids.first().unwrap_or_log(),
1332                peer.peer_address,
1333                format_opt_ts(peer.peer_stats.latency.map(|l| l.average)),
1334                format_opt_bps(Some(peer.peer_stats.transfer.down.average)),
1335                format_opt_bps(Some(peer.peer_stats.transfer.up.average)),
1336            ));
1337        }
1338
1339        // Dump connection table
1340        let connman =
1341            if let Some(connection_manager) = registry.network_manager().opt_connection_manager() {
1342                connection_manager.debug_print()
1343            } else {
1344                "Connection manager unavailable when detached".to_owned()
1345            };
1346
1347        Ok(format!(
1348            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
1349            nodeinfo_rtab,
1350            nodeinfo_net,
1351            nodeinfo_rpc,
1352            peertable.join("\n"),
1353            connman,
1354            nodeinfo_crypto,
1355            nodeinfo_attach
1356        ))
1357    }
1358
1359    fn debug_nodeid(&self) -> VeilidAPIResult<String> {
1360        // Dump routing table entry
1361        let registry = self.core_context()?.registry();
1362        let nodeid = registry.routing_table().debug_info_nodeid();
1363        Ok(nodeid)
1364    }
1365
1366    #[expect(clippy::unused_async)]
1367    async fn debug_config(
1368        &self,
1369        arg_0: Option<String>,
1370        arg_1: Option<String>,
1371    ) -> VeilidAPIResult<String> {
1372        let mut args = arg_0.as_deref().unwrap_or_default();
1373        let mut config = self.config()?;
1374        if !args.starts_with("insecure") {
1375            if let Some(extra) = arg_1 {
1376                apibail_invalid_argument!("debug_config", "arg_1", extra);
1377            }
1378            config = config.safe();
1379        } else {
1380            if args != "insecure" {
1381                apibail_invalid_argument!("debug_config", "arg_0", args);
1382            }
1383            args = arg_1.as_deref().unwrap_or_default();
1384        }
1385        let args = args.trim_start();
1386
1387        if args.is_empty() {
1388            return config.get_key_json("", true);
1389        }
1390        config.get_key_json(args, true)
1391    }
1392
1393    async fn debug_network(
1394        &self,
1395        command: Option<DebugNetworkSubcommand>,
1396    ) -> VeilidAPIResult<String> {
1397        let Some(command) = command else {
1398            apibail_missing_argument!("debug_network", "arg_0");
1399        };
1400
1401        match command {
1402            DebugNetworkSubcommand::Restart => {
1403                // Must be attached
1404                if matches!(
1405                    self.get_state().await?.attachment.state,
1406                    AttachmentState::Detached
1407                ) {
1408                    apibail_internal!("Must be attached to restart network");
1409                }
1410
1411                let registry = self.core_context()?.registry();
1412                registry.network_manager().restart_network();
1413
1414                Ok("Network restarted".to_owned())
1415            }
1416            DebugNetworkSubcommand::Stats => {
1417                let registry = self.core_context()?.registry();
1418                let debug_stats = registry.network_manager().debug();
1419
1420                Ok(debug_stats)
1421            }
1422        }
1423    }
1424
1425    async fn debug_purge(&self, command: Option<DebugPurgeSubcommand>) -> VeilidAPIResult<String> {
1426        let registry = self.core_context()?.registry();
1427
1428        let Some(command) = command else {
1429            apibail_missing_argument!("debug_purge", "parameter");
1430        };
1431
1432        match command {
1433            DebugPurgeSubcommand::Buckets => {
1434                // Must be detached
1435                if !matches!(
1436                    self.get_state().await?.attachment.state,
1437                    AttachmentState::Detached | AttachmentState::Detaching
1438                ) {
1439                    apibail_internal!("Must be detached to purge");
1440                }
1441                match registry.routing_table().purge_buckets().await {
1442                    Ok(_) => Ok("Buckets purged".to_owned()),
1443                    Err(e) => Ok(format!("{}", e)),
1444                }
1445            }
1446            DebugPurgeSubcommand::Connections => {
1447                // Purge connection table
1448                let opt_connection_manager = registry.network_manager().opt_connection_manager();
1449
1450                if let Some(connection_manager) = &opt_connection_manager {
1451                    connection_manager.shutdown().await;
1452                }
1453
1454                // Eliminate last_connections from routing table entries
1455                registry.routing_table().purge_last_connections();
1456
1457                if let Some(connection_manager) = &opt_connection_manager {
1458                    connection_manager
1459                        .startup()
1460                        .map_err(VeilidAPIError::internal)?;
1461                }
1462                Ok("Connections purged".to_owned())
1463            }
1464            DebugPurgeSubcommand::Routes => {
1465                // Purge route spec store
1466                self.with_debug_cache(|dc| {
1467                    dc.imported_routes.clear();
1468                });
1469                match registry.routing_table().route_spec_store().purge().await {
1470                    Ok(_) => Ok("Routes purged".to_owned()),
1471                    Err(e) => Ok(format!("{}", e)),
1472                }
1473            }
1474        }
1475    }
1476
1477    async fn debug_attach(&self) -> VeilidAPIResult<String> {
1478        if !matches!(
1479            self.get_state().await?.attachment.state,
1480            AttachmentState::Detached
1481        ) {
1482            apibail_internal!("Not detached");
1483        }
1484
1485        self.attach().await?;
1486
1487        Ok("Attached".to_owned())
1488    }
1489
1490    async fn debug_detach(&self) -> VeilidAPIResult<String> {
1491        if matches!(
1492            self.get_state().await?.attachment.state,
1493            AttachmentState::Detaching
1494        ) {
1495            apibail_internal!("Not attached");
1496        };
1497
1498        self.detach().await?;
1499
1500        Ok("Detached".to_owned())
1501    }
1502
1503    fn debug_contact(&self, node_ref: String) -> VeilidAPIResult<String> {
1504        let registry = self.core_context()?.registry();
1505
1506        let node_ref = get_debug_argument(
1507            &node_ref,
1508            "debug_contact",
1509            "node_ref",
1510            get_filtered_node_ref(registry.clone()),
1511        )?;
1512
1513        let cm = registry
1514            .network_manager()
1515            .get_node_contact_method(node_ref)
1516            .map_err(VeilidAPIError::internal)?;
1517
1518        Ok(format!("{:#?}", cm))
1519    }
1520
1521    async fn debug_resolve(&self, destination: String) -> VeilidAPIResult<String> {
1522        let registry = self.core_context()?.registry();
1523        if !registry.attachment_manager().is_attached() {
1524            apibail_internal!("Must be attached first");
1525        };
1526
1527        let dest = async_get_debug_argument(
1528            &destination,
1529            "debug_resolve",
1530            "destination",
1531            self.clone().get_destination(registry.clone()),
1532        )
1533        .await?;
1534
1535        let routing_table = registry.routing_table();
1536        match &dest {
1537            Destination::Direct {
1538                node,
1539                safety_selection: _,
1540            } => Ok(format!(
1541                "Destination: {:#?}\nNode:\n{}\n",
1542                &dest,
1543                routing_table.debug_info_entry(node.unfiltered())
1544            )),
1545            Destination::DialInfo {
1546                dial_info: relay_di,
1547                node,
1548            } => Ok(format!(
1549                "Destination: {:#?}\nNode:\n{}\nDialInfo:\n{}\n",
1550                &dest,
1551                routing_table.debug_info_entry(node.clone()),
1552                relay_di,
1553            )),
1554            Destination::PrivateRoute {
1555                private_route: _,
1556                safety_selection: _,
1557            } => Ok(format!("Destination: {:#?}", &dest)),
1558        }
1559    }
1560
1561    async fn debug_ping(&self, destination: String) -> VeilidAPIResult<String> {
1562        let registry = self.core_context()?.registry();
1563        if !registry.attachment_manager().is_attached() {
1564            apibail_internal!("Must be attached first");
1565        };
1566
1567        let dest = async_get_debug_argument(
1568            &destination,
1569            "debug_ping",
1570            "destination",
1571            self.clone().get_destination(registry.clone()),
1572        )
1573        .await?;
1574
1575        // Send a StatusQ
1576        let rpc_processor = registry.rpc_processor();
1577        let result = Box::pin(rpc_processor.rpc_call_status(dest))
1578            .await
1579            .map_err(VeilidAPIError::internal)?;
1580        match result {
1581            StatusResult::Answer { answer, .. } => Ok(format!("{:#?}", answer)),
1582            StatusResult::Failed(sdr) => Ok(format!("send failed: {}", sdr)),
1583            StatusResult::NotSent(nr) => Ok(format!("not sent: {:?}", nr)),
1584        }
1585    }
1586
1587    async fn debug_app_message(
1588        &self,
1589        destination: String,
1590        data_parts: Vec<String>,
1591    ) -> VeilidAPIResult<String> {
1592        let registry = self.core_context()?.registry();
1593        if !registry.attachment_manager().is_attached() {
1594            apibail_internal!("Must be attached first");
1595        };
1596        let dest = async_get_debug_argument(
1597            &destination,
1598            "debug_app_message",
1599            "destination",
1600            self.clone().get_destination(registry.clone()),
1601        )
1602        .await?;
1603
1604        let data_text = join_args(data_parts);
1605        let data = get_debug_argument(&data_text, "debug_app_message", "data", get_data)?;
1606        let data_len = data.len();
1607
1608        // Send an AppMessage
1609        let rpc_processor = registry.rpc_processor();
1610
1611        let out = match rpc_processor
1612            .rpc_call_app_message(dest, data.into())
1613            .await
1614            .map_err(VeilidAPIError::internal)?
1615        {
1616            NetworkResult::Value(_) => format!("Sent {} bytes", data_len),
1617            r => {
1618                return Ok(r.to_string());
1619            }
1620        };
1621
1622        Ok(out)
1623    }
1624
1625    async fn debug_app_call(
1626        &self,
1627        destination: String,
1628        data_parts: Vec<String>,
1629    ) -> VeilidAPIResult<String> {
1630        let registry = self.core_context()?.registry();
1631        if !registry.attachment_manager().is_attached() {
1632            apibail_internal!("Must be attached first");
1633        };
1634        let dest = async_get_debug_argument(
1635            &destination,
1636            "debug_app_call",
1637            "destination",
1638            self.clone().get_destination(registry.clone()),
1639        )
1640        .await?;
1641
1642        let data_text = join_args(data_parts);
1643        let data = get_debug_argument(&data_text, "debug_app_call", "data", get_data)?;
1644        let data_len = data.len();
1645
1646        // Send an AppCall
1647        let rpc_processor = registry.rpc_processor();
1648
1649        let out = match Box::pin(rpc_processor.rpc_call_app_call(dest, data.into()))
1650            .await
1651            .map_err(VeilidAPIError::internal)?
1652        {
1653            NetworkResult::Value(v) => format!(
1654                "Sent {} bytes, received: {}",
1655                data_len,
1656                human_byte_data(&v.answer, Some(512))
1657            ),
1658            r => {
1659                return Ok(r.to_string());
1660            }
1661        };
1662
1663        Ok(out)
1664    }
1665
1666    async fn debug_app_reply(
1667        &self,
1668        id: Option<String>,
1669        data_parts: Vec<String>,
1670    ) -> VeilidAPIResult<String> {
1671        let registry = self.core_context()?.registry();
1672        if !registry.attachment_manager().is_attached() {
1673            apibail_internal!("Must be attached first");
1674        };
1675
1676        let (call_id, data) = if let Some(id) = id {
1677            let stripped_id = id.strip_prefix('#').unwrap_or(id.as_str());
1678            let call_id = OperationId::new(
1679                u64::from_str_radix(stripped_id, 16)
1680                    .map_err(|e| VeilidAPIError::parse_error(e.to_string(), stripped_id))?,
1681            );
1682            let data_text = join_args(data_parts);
1683            let data = get_debug_argument(&data_text, "debug_app_reply", "data", get_data)?;
1684            (call_id, data)
1685        } else {
1686            let rpc_processor = registry.rpc_processor();
1687
1688            let call_id = rpc_processor
1689                .get_app_call_ids()
1690                .first()
1691                .cloned()
1692                .ok_or_else(|| VeilidAPIError::generic("no app calls waiting"))?;
1693            let data_text = join_args(data_parts);
1694            let data = get_debug_argument(&data_text, "debug_app_reply", "data", get_data)?;
1695            (call_id, data)
1696        };
1697
1698        let data_len = data.len();
1699
1700        // Send a AppCall Reply
1701        self.app_call_reply(call_id, data)
1702            .await
1703            .map_err(VeilidAPIError::internal)?;
1704
1705        Ok(format!("Replied with {} bytes", data_len))
1706    }
1707
1708    async fn debug_route_allocate(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1709        // [uno|ord|ord!] [rel] [<count>] [in|out] [avoid_node_id]
1710
1711        let registry = self.core_context()?.registry();
1712        let routing_table = registry.routing_table();
1713        let rss = routing_table.route_spec_store();
1714        let default_route_hop_count = self.config()?.network.rpc.default_route_hop_count as usize;
1715
1716        let mut ai = 0;
1717        let mut sequencing = Sequencing::default();
1718        let mut stability = Stability::default();
1719        let mut hop_count = default_route_hop_count;
1720        let mut directions = DirectionSet::all();
1721
1722        while ai < args.len() {
1723            if let Ok(seq) =
1724                get_debug_argument_at(&args, ai, "debug_route", "sequencing", get_sequencing)
1725            {
1726                sequencing = seq;
1727            } else if let Ok(sta) =
1728                get_debug_argument_at(&args, ai, "debug_route", "stability", get_stability)
1729            {
1730                stability = sta;
1731            } else if let Ok(hc) =
1732                get_debug_argument_at(&args, ai, "debug_route", "hop_count", get_number)
1733            {
1734                hop_count = hc;
1735            } else if let Ok(ds) =
1736                get_debug_argument_at(&args, ai, "debug_route", "direction_set", get_direction_set)
1737            {
1738                directions = ds;
1739            } else {
1740                return Ok(format!("Invalid argument specified: {}", args[ai]));
1741            }
1742            ai += 1;
1743        }
1744
1745        // Allocate route
1746        let allocate_route_params = AllocateRouteParams {
1747            crypto_kinds: VALID_CRYPTO_KINDS.to_vec(),
1748            hop_count,
1749            stability,
1750            sequencing,
1751            directions,
1752            avoid_nodes: Vec::new(),
1753            automatic: false,
1754        };
1755        let out = match rss.allocate_route(allocate_route_params).await {
1756            Ok(v) => v.route_id.to_string(),
1757            Err(e) => format!("Route allocation failed: {}", e),
1758        };
1759
1760        Ok(out)
1761    }
1762    fn debug_route_release(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1763        // <route id>
1764        let registry = self.core_context()?.registry();
1765        let routing_table = registry.routing_table();
1766        let rss = routing_table.route_spec_store();
1767
1768        let route_id = get_debug_argument_at(
1769            &args,
1770            0,
1771            "debug_route",
1772            "route_id",
1773            get_route_id(registry.clone(), true, true),
1774        )?;
1775
1776        // Release route
1777        let out = match rss.release_route(route_id.clone()) {
1778            true => {
1779                // release imported
1780                self.with_debug_cache(|dc| {
1781                    for (n, ir) in dc.imported_routes.iter().enumerate() {
1782                        if *ir == route_id {
1783                            let _ = dc.imported_routes.remove(n);
1784                            break;
1785                        }
1786                    }
1787                });
1788                "Released".to_owned()
1789            }
1790            false => "Route does not exist".to_owned(),
1791        };
1792
1793        Ok(out)
1794    }
1795    async fn debug_route_publish(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1796        // <route id> [full]
1797        let registry = self.core_context()?.registry();
1798        let routing_table = registry.routing_table();
1799        let rss = routing_table.route_spec_store();
1800
1801        let route_id = get_debug_argument_at(
1802            &args,
1803            0,
1804            "debug_route",
1805            "route_id",
1806            get_route_id(registry.clone(), true, false),
1807        )?;
1808        let full = {
1809            if args.len() > 1 {
1810                let full_val = get_debug_argument_at(&args, 1, "debug_route", "full", get_string)?
1811                    .to_ascii_lowercase();
1812                if full_val == "full" {
1813                    true
1814                } else {
1815                    apibail_invalid_argument!("debug_route", "full", full_val);
1816                }
1817            } else {
1818                false
1819            }
1820        };
1821
1822        // Publish route
1823        let arsid = AllocatedRouteSetId::from_route_id(route_id.clone());
1824        let out = match rss.assemble_private_route_set(&arsid, Some(!full)).await {
1825            Ok(private_routes) => {
1826                if let Err(e) = rss.mark_route_published(&arsid, true) {
1827                    return Ok(format!("Couldn't mark route published: {}", e));
1828                }
1829                // Convert to blob
1830                let blob_data = RouteSpecStore::private_routes_to_blob(&private_routes)
1831                    .map_err(VeilidAPIError::internal)?;
1832                let out = BASE64URL_NOPAD.encode(&blob_data);
1833                veilid_log!(registry info
1834                    "Published route {} as {} bytes:\n{}",
1835                    route_id,
1836                    blob_data.len(),
1837                    out
1838                );
1839                format!("Published route {}", route_id)
1840            }
1841            Err(e) => {
1842                format!("Couldn't assemble private route: {}", e)
1843            }
1844        };
1845
1846        Ok(out)
1847    }
1848    fn debug_route_unpublish(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1849        // <route id>
1850        let registry = self.core_context()?.registry();
1851        let routing_table = registry.routing_table();
1852        let rss = routing_table.route_spec_store();
1853
1854        let route_id = get_debug_argument_at(
1855            &args,
1856            0,
1857            "debug_route",
1858            "route_id",
1859            get_route_id(registry.clone(), true, false),
1860        )?;
1861
1862        // Unpublish route
1863        let arsid = AllocatedRouteSetId::from_route_id(route_id);
1864        let out = if let Err(e) = rss.mark_route_published(&arsid, false) {
1865            return Ok(format!("Couldn't mark route unpublished: {}", e));
1866        } else {
1867            "Route unpublished".to_owned()
1868        };
1869        Ok(out)
1870    }
1871    fn debug_route_print(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1872        // <route id>
1873        let registry = self.core_context()?.registry();
1874        let routing_table = registry.routing_table();
1875        let rss = routing_table.route_spec_store();
1876
1877        if let Ok(route_id) = get_debug_argument_at(
1878            &args,
1879            0,
1880            "debug_route",
1881            "route_id",
1882            get_route_id(registry.clone(), true, true),
1883        ) {
1884            // Content (persisted spec) view plus the cache view with live route stats
1885            Ok(format!(
1886                "{}\n{}",
1887                rss.debug_route_by_id(&route_id),
1888                rss.display_route_by_id(&route_id)
1889            ))
1890        } else if let Ok(route_key) = get_debug_argument_at(
1891            &args,
1892            0,
1893            "debug_route",
1894            "route_key",
1895            get_route_key(registry.clone(), true, true),
1896        ) {
1897            Ok(rss.debug_route_by_key(&route_key))
1898        } else {
1899            Ok("Route key not found".to_string())
1900        }
1901    }
1902    fn debug_route_list(&self, _args: Vec<String>) -> VeilidAPIResult<String> {
1903        //
1904        let registry = self.core_context()?.registry();
1905        let routing_table = registry.routing_table();
1906        let rss = routing_table.route_spec_store();
1907
1908        let routes = rss.list_allocated_routes(|k, v| Some((k.clone(), format!("{:#}", v))));
1909        let mut out = format!("Allocated Routes: (count = {}):\n", routes.len());
1910        for (r, s) in routes {
1911            out.push_str(&format!("{}: {}\n", r, s));
1912        }
1913
1914        let remote_routes = rss.list_remote_routes(|k, v| Some((k.clone(), format!("{:#}", v))));
1915        out.push_str(&format!(
1916            "Remote Routes: (count = {}):\n",
1917            remote_routes.len()
1918        ));
1919        for (r, s) in remote_routes {
1920            out.push_str(&format!("{}: {:#}\n", r, s));
1921        }
1922
1923        Ok(out)
1924    }
1925    fn debug_route_import(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1926        // <blob>
1927        let registry = self.core_context()?.registry();
1928        let routing_table = registry.routing_table();
1929        let rss = routing_table.route_spec_store();
1930
1931        let blob = get_debug_argument_at(&args, 0, "debug_route", "blob", get_string)?;
1932        let blob_dec = BASE64URL_NOPAD
1933            .decode(blob.as_bytes())
1934            .map_err(|e| VeilidAPIError::parse_error(e.to_string(), blob))?;
1935
1936        let route_id = rss.import_remote_route_blob(blob_dec)?;
1937
1938        let out = self.with_debug_cache(|dc| {
1939            let n = dc.imported_routes.len();
1940            let out = format!("Private route #{} imported: {}", n, route_id);
1941            dc.imported_routes.push(route_id);
1942            out
1943        });
1944
1945        Ok(out)
1946    }
1947
1948    async fn debug_route_test(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1949        // <route id>
1950        let registry = self.core_context()?.registry();
1951        let routing_table = registry.routing_table();
1952        let rss = routing_table.route_spec_store();
1953
1954        let route_id = get_debug_argument_at(
1955            &args,
1956            0,
1957            "debug_route",
1958            "route_id",
1959            get_route_id(registry.clone(), true, true),
1960        )?;
1961
1962        let success = rss.test_route(route_id).await?;
1963
1964        let out = match success {
1965            Some(true) => "SUCCESS".to_owned(),
1966            Some(false) => "FAILED".to_owned(),
1967            None => "UNTESTED".to_owned(),
1968        };
1969
1970        Ok(out)
1971    }
1972
1973    async fn debug_route(&self, command: Option<DebugRouteSubcommand>) -> VeilidAPIResult<String> {
1974        let Some(command) = command else {
1975            apibail_missing_argument!("debug_route", "command");
1976        };
1977
1978        match command {
1979            DebugRouteSubcommand::Allocate { params } => self.debug_route_allocate(params).await,
1980            DebugRouteSubcommand::Release { route_id } => self.debug_route_release(vec![route_id]),
1981            DebugRouteSubcommand::Publish { route_id, full } => {
1982                let mut args = vec![route_id];
1983                if let Some(full) = full {
1984                    args.push(full);
1985                }
1986                self.debug_route_publish(args).await
1987            }
1988            DebugRouteSubcommand::Unpublish { route_id } => {
1989                self.debug_route_unpublish(vec![route_id])
1990            }
1991            DebugRouteSubcommand::Print { route } => self.debug_route_print(vec![route]),
1992            DebugRouteSubcommand::List => self.debug_route_list(vec![]),
1993            DebugRouteSubcommand::Import { blob } => self.debug_route_import(vec![blob]),
1994            DebugRouteSubcommand::Test { route_id } => self.debug_route_test(vec![route_id]).await,
1995        }
1996    }
1997
1998    fn debug_record_list(&self, args: Vec<String>) -> VeilidAPIResult<String> {
1999        // <local|remote>
2000        let registry = self.core_context()?.registry();
2001        let storage_manager = registry.storage_manager();
2002
2003        let scope = get_debug_argument_at(&args, 0, "debug_record_list", "scope", get_string)?;
2004        let out = match scope.as_str() {
2005            "local" => {
2006                let mut out = "Local Records:\n".to_string();
2007                out += &storage_manager.debug_local_records();
2008                out
2009            }
2010            "remote" => {
2011                let mut out = "Remote Records:\n".to_string();
2012                out += &storage_manager.debug_remote_records();
2013                out
2014            }
2015            "opened" => {
2016                let mut out = "Opened Records:\n".to_string();
2017                out += &storage_manager.debug_opened_records();
2018                out
2019            }
2020            "watched" => {
2021                let mut out = "Watched Records:\n".to_string();
2022                out += &storage_manager.debug_watched_records();
2023                out
2024            }
2025            "offline" => {
2026                let mut out = "Offline Records:\n".to_string();
2027                out += &storage_manager.debug_offline_records();
2028                out
2029            }
2030            "transactions" => {
2031                let mut out = "Record Transaction:\n".to_string();
2032                out += &storage_manager.debug_transactions();
2033                out
2034            }
2035            _ => "Invalid scope\n".to_owned(),
2036        };
2037        Ok(out)
2038    }
2039
2040    async fn debug_record_purge_bytes(
2041        &self,
2042        scope: String,
2043        bytes: Option<u64>,
2044    ) -> VeilidAPIResult<String> {
2045        // <local|remote> [bytes]
2046        let registry = self.core_context()?.registry();
2047        let storage_manager = registry.storage_manager();
2048
2049        self.with_debug_cache(|dc| {
2050            dc.opened_record_contexts.clear();
2051        });
2052        storage_manager.close_all_records().await?;
2053
2054        let out = match scope.as_str() {
2055            "local" => storage_manager.purge_local_records_by_bytes(bytes).await,
2056            "remote" => storage_manager.purge_remote_records_by_bytes(bytes).await,
2057            _ => "Invalid scope\n".to_owned(),
2058        };
2059        Ok(out)
2060    }
2061
2062    async fn debug_record_purge_keys(
2063        &self,
2064        scope: String,
2065        keys: Vec<String>,
2066    ) -> VeilidAPIResult<String> {
2067        // <local|remote> [bytes]
2068        let registry = self.core_context()?.registry();
2069        let storage_manager = registry.storage_manager();
2070
2071        self.with_debug_cache(|dc| {
2072            dc.opened_record_contexts.clear();
2073        });
2074        storage_manager.close_all_records().await?;
2075
2076        let keys = keys
2077            .iter()
2078            .map(|k| {
2079                RecordKey::from_str(k)
2080                    .map(|rk| rk.opaque())
2081                    .or_else(|_| OpaqueRecordKey::from_str(k))
2082            })
2083            .collect::<Result<Vec<OpaqueRecordKey>, VeilidAPIError>>()?;
2084
2085        let out = match scope.as_str() {
2086            "local" => storage_manager.purge_local_records_by_keys(keys).await,
2087            "remote" => storage_manager.purge_remote_records_by_keys(keys).await,
2088            _ => "Invalid scope\n".to_owned(),
2089        };
2090        Ok(out)
2091    }
2092
2093    async fn debug_record_create(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2094        let crypto = self.crypto()?;
2095
2096        let schema = get_debug_argument_at(
2097            &args,
2098            0,
2099            "debug_record_create",
2100            "dht_schema",
2101            get_dht_schema,
2102        )
2103        .unwrap_or_else(|_| Ok(DHTSchema::default()))?;
2104
2105        let csv = get_debug_argument_at(
2106            &args,
2107            1,
2108            "debug_record_create",
2109            "kind",
2110            get_crypto_system_version(&crypto),
2111        )
2112        .unwrap_or_else(|_| crypto.best());
2113
2114        let ss = get_debug_argument_at(
2115            &args,
2116            2,
2117            "debug_record_create",
2118            "safety_selection",
2119            get_safety_selection(self.core_context()?.registry()),
2120        )
2121        .ok();
2122
2123        // Get routing context with optional safety
2124        let rc = self.routing_context()?;
2125        let rc = if let Some(ss) = ss {
2126            match rc.with_safety(ss) {
2127                Err(e) => return Ok(format!("Can't use safety selection: {}", e)),
2128                Ok(v) => v,
2129            }
2130        } else {
2131            rc
2132        };
2133
2134        // Do a record create
2135        let record = match rc.create_dht_record(csv.kind(), schema, None).await {
2136            Err(e) => return Ok(format!("Can't open DHT record: {}", e)),
2137            Ok(v) => v,
2138        };
2139
2140        // Save routing context for record
2141        self.with_debug_cache(|dc| {
2142            dc.opened_record_contexts.insert(record.key(), rc);
2143        });
2144
2145        Ok(format!(
2146            "Created: {} {}\n{:?}",
2147            record.key(),
2148            record.owner_keypair().unwrap_or_log(),
2149            record
2150        ))
2151    }
2152
2153    async fn debug_record_open(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2154        let registry = self.core_context()?.registry();
2155
2156        let (key, ss) = get_debug_argument_at(
2157            &args,
2158            0,
2159            "debug_record_open",
2160            "key",
2161            get_dht_key(registry.clone()),
2162        )?;
2163        let writer =
2164            get_debug_argument_at(&args, 1, "debug_record_open", "writer", get_keypair).ok();
2165
2166        // Get routing context with optional safety
2167        let rc = self.routing_context()?;
2168        let rc = if let Some(ss) = ss {
2169            match rc.with_safety(ss) {
2170                Err(e) => return Ok(format!("Can't use safety selection: {}", e)),
2171                Ok(v) => v,
2172            }
2173        } else {
2174            rc
2175        };
2176
2177        // Do a record open
2178        let record = match rc.open_dht_record(key.clone(), writer).await {
2179            Err(e) => return Ok(format!("Can't open DHT record: {}", e)),
2180            Ok(v) => v,
2181        };
2182
2183        // Save routing context for record
2184        self.with_debug_cache(|dc| {
2185            dc.opened_record_contexts.insert(record.key(), rc);
2186        });
2187
2188        Ok(format!("Opened: {}\n{:#?}", key, record))
2189    }
2190
2191    async fn debug_record_close(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2192        let (key, rc) =
2193            self.clone()
2194                .get_opened_dht_record_context(&args, "debug_record_close", "key", 0)?;
2195
2196        // Do a record close
2197        if let Err(e) = rc.close_dht_record(key.clone()).await {
2198            return Ok(format!("Can't close DHT record: {}", e));
2199        };
2200
2201        Ok(format!("Closed: {:?}", key))
2202    }
2203
2204    async fn debug_record_set(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2205        let mut opt_arg_add = if !args.is_empty() && get_dht_key_no_safety(&args[0]).is_some() {
2206            1
2207        } else {
2208            0
2209        };
2210        let (key, rc) =
2211            self.clone()
2212                .get_opened_dht_record_context(&args, "debug_record_set", "key", 0)?;
2213        let subkey = get_debug_argument_at(
2214            &args,
2215            opt_arg_add,
2216            "debug_record_set",
2217            "subkey",
2218            get_number::<u32>,
2219        )?;
2220        let data =
2221            get_debug_argument_at(&args, 1 + opt_arg_add, "debug_record_set", "data", get_data)?;
2222        let writer = match get_debug_argument_at(
2223            &args,
2224            2 + opt_arg_add,
2225            "debug_record_set",
2226            "writer",
2227            get_keypair,
2228        ) {
2229            Ok(v) => {
2230                opt_arg_add += 1;
2231                Some(v)
2232            }
2233            Err(_) => None,
2234        };
2235        let allow_offline = if args.len() > 2 + opt_arg_add {
2236            get_debug_argument_at(
2237                &args,
2238                2 + opt_arg_add,
2239                "debug_record_set",
2240                "allow_offline",
2241                get_string,
2242            )
2243            .ok()
2244        } else {
2245            None
2246        };
2247
2248        let allow_offline = if let Some(allow_offline) = allow_offline {
2249            if &allow_offline == "online" || &allow_offline == "false" {
2250                Some(AllowOffline(false))
2251            } else if &allow_offline == "offline" || &allow_offline == "true" {
2252                Some(AllowOffline(true))
2253            } else {
2254                return Ok(format!("Unknown allow_offline: {}", allow_offline));
2255            }
2256        } else {
2257            None
2258        };
2259
2260        // Do a record set
2261        let value = match rc
2262            .set_dht_value(
2263                key,
2264                subkey as ValueSubkey,
2265                data,
2266                Some(SetDHTValueOptions {
2267                    writer,
2268                    allow_offline,
2269                }),
2270            )
2271            .await
2272        {
2273            Err(e) => {
2274                return Ok(format!("Can't set DHT value: {}", e));
2275            }
2276            Ok(v) => v,
2277        };
2278        let out = if let Some(value) = value {
2279            format!("Newer value found: {:?}", value)
2280        } else {
2281            "Success".to_owned()
2282        };
2283        Ok(out)
2284    }
2285
2286    async fn debug_record_get(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2287        let opt_arg_add = if !args.is_empty() && get_dht_key_no_safety(&args[0]).is_some() {
2288            1
2289        } else {
2290            0
2291        };
2292
2293        let (key, rc) =
2294            self.clone()
2295                .get_opened_dht_record_context(&args, "debug_record_get", "key", 0)?;
2296        let subkey = get_debug_argument_at(
2297            &args,
2298            opt_arg_add,
2299            "debug_record_get",
2300            "subkey",
2301            get_number::<u32>,
2302        )?;
2303        let force_refresh = if args.len() > 1 + opt_arg_add {
2304            Some(get_debug_argument_at(
2305                &args,
2306                1 + opt_arg_add,
2307                "debug_record_get",
2308                "force_refresh",
2309                get_string,
2310            )?)
2311        } else {
2312            None
2313        };
2314
2315        let force_refresh = if let Some(force_refresh) = force_refresh {
2316            if &force_refresh == "force" {
2317                true
2318            } else {
2319                return Ok(format!("Unknown force: {}", force_refresh));
2320            }
2321        } else {
2322            false
2323        };
2324
2325        // Do a record get
2326        let value = match rc
2327            .get_dht_value(key, subkey as ValueSubkey, force_refresh)
2328            .await
2329        {
2330            Err(e) => {
2331                return Ok(format!("Can't get DHT value: {}", e));
2332            }
2333            Ok(v) => v,
2334        };
2335        let out = if let Some(value) = value {
2336            format!("{:?}", value)
2337        } else {
2338            "No value data returned".to_owned()
2339        };
2340        Ok(out)
2341    }
2342
2343    async fn debug_record_delete(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2344        let key = get_debug_argument_at(
2345            &args,
2346            0,
2347            "debug_record_delete",
2348            "key",
2349            get_dht_key_no_safety,
2350        )?;
2351
2352        // Do a record delete (can use any routing context here)
2353        let rc = self.routing_context()?;
2354        match rc.delete_dht_record(key).await {
2355            Err(e) => return Ok(format!("Can't delete DHT record: {}", e)),
2356            Ok(v) => v,
2357        };
2358        Ok("DHT record deleted".to_string())
2359    }
2360
2361    async fn debug_record_info(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2362        let registry = self.core_context()?.registry();
2363        let storage_manager = registry.storage_manager();
2364
2365        let key =
2366            get_debug_argument_at(&args, 0, "debug_record_info", "key", get_dht_key_no_safety)?;
2367
2368        let subkey = get_debug_argument_at(
2369            &args,
2370            1,
2371            "debug_record_info",
2372            "subkey",
2373            get_number::<ValueSubkey>,
2374        )
2375        .ok();
2376
2377        let out = if let Some(subkey) = subkey {
2378            let li = storage_manager
2379                .debug_local_record_subkey_info(key.clone(), subkey)
2380                .await;
2381            let ri = storage_manager
2382                .debug_remote_record_subkey_info(key.clone(), subkey)
2383                .await;
2384            format!(
2385                "Local Subkey Info:\n{}\n\nRemote Subkey Info:\n{}\n",
2386                li, ri
2387            )
2388        } else {
2389            let li = storage_manager.debug_local_record_info(key.clone());
2390            let ri = storage_manager.debug_remote_record_info(key.clone());
2391            format!("Local Info:\n{}\n\nRemote Info:\n{}\n", li, ri)
2392        };
2393        Ok(out)
2394    }
2395
2396    async fn debug_record_watch(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2397        let opt_arg_add = if !args.is_empty() && get_dht_key_no_safety(&args[0]).is_some() {
2398            1
2399        } else {
2400            0
2401        };
2402
2403        let (key, rc) =
2404            self.clone()
2405                .get_opened_dht_record_context(&args, "debug_record_watch", "key", 0)?;
2406
2407        let mut rest_defaults = false;
2408        let subkeys = get_debug_argument_at(
2409            &args,
2410            opt_arg_add,
2411            "debug_record_watch",
2412            "subkeys",
2413            get_subkeys,
2414        )
2415        .ok()
2416        .map(Some)
2417        .unwrap_or_else(|| {
2418            rest_defaults = true;
2419            None
2420        });
2421
2422        let opt_expiration = if rest_defaults {
2423            None
2424        } else {
2425            get_debug_argument_at(
2426                &args,
2427                1 + opt_arg_add,
2428                "debug_record_watch",
2429                "expiration",
2430                parse_duration,
2431            )
2432            .ok()
2433            .map(|dur| {
2434                if dur == 0 {
2435                    None
2436                } else {
2437                    Some(Timestamp::now_non_decreasing().later(TimestampDuration::new(dur)))
2438                }
2439            })
2440            .unwrap_or_else(|| {
2441                rest_defaults = true;
2442                None
2443            })
2444        };
2445        let count = if rest_defaults {
2446            None
2447        } else {
2448            get_debug_argument_at(
2449                &args,
2450                2 + opt_arg_add,
2451                "debug_record_watch",
2452                "count",
2453                get_number,
2454            )
2455            .ok()
2456            .map(Some)
2457            .unwrap_or_else(|| {
2458                rest_defaults = true;
2459                Some(u32::MAX)
2460            })
2461        };
2462
2463        // Do a record watch
2464        let active = match rc
2465            .watch_dht_values(key, subkeys, opt_expiration, count)
2466            .await
2467        {
2468            Err(e) => {
2469                return Ok(format!("Can't watch DHT value: {}", e));
2470            }
2471            Ok(v) => v,
2472        };
2473        if !active {
2474            return Ok("Failed to watch value".to_owned());
2475        }
2476        Ok("Success".to_owned())
2477    }
2478
2479    async fn debug_record_cancel(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2480        let opt_arg_add = if !args.is_empty() && get_dht_key_no_safety(&args[0]).is_some() {
2481            1
2482        } else {
2483            0
2484        };
2485
2486        let (key, rc) =
2487            self.clone()
2488                .get_opened_dht_record_context(&args, "debug_record_watch", "key", 0)?;
2489        let subkeys = get_debug_argument_at(
2490            &args,
2491            opt_arg_add,
2492            "debug_record_watch",
2493            "subkeys",
2494            get_subkeys,
2495        )
2496        .ok();
2497
2498        // Do a record watch cancel
2499        let still_active = match rc.cancel_dht_watch(key, subkeys).await {
2500            Err(e) => {
2501                return Ok(format!("Can't cancel DHT watch: {}", e));
2502            }
2503            Ok(v) => v,
2504        };
2505
2506        Ok(if still_active {
2507            "Watch partially cancelled".to_owned()
2508        } else {
2509            "Watch cancelled".to_owned()
2510        })
2511    }
2512
2513    async fn debug_record_inspect(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2514        let opt_arg_add = if !args.is_empty() && get_dht_key_no_safety(&args[0]).is_some() {
2515            1
2516        } else {
2517            0
2518        };
2519
2520        let (key, rc) =
2521            self.clone()
2522                .get_opened_dht_record_context(&args, "debug_record_inspect", "key", 0)?;
2523
2524        let mut rest_defaults = false;
2525
2526        let scope = if rest_defaults {
2527            Default::default()
2528        } else {
2529            get_debug_argument_at(
2530                &args,
2531                opt_arg_add,
2532                "debug_record_inspect",
2533                "scope",
2534                get_dht_report_scope,
2535            )
2536            .ok()
2537            .unwrap_or_else(|| {
2538                rest_defaults = true;
2539                Default::default()
2540            })
2541        };
2542
2543        let subkeys = if rest_defaults {
2544            None
2545        } else {
2546            get_debug_argument_at(
2547                &args,
2548                1 + opt_arg_add,
2549                "debug_record_inspect",
2550                "subkeys",
2551                get_subkeys,
2552            )
2553            .ok()
2554        };
2555
2556        // Do a record inspect
2557        let report = match rc.inspect_dht_record(key, subkeys, scope).await {
2558            Err(e) => {
2559                return Ok(format!("Can't inspect DHT record: {}", e));
2560            }
2561            Ok(v) => v,
2562        };
2563
2564        Ok(format!("Success: report={:?}", report))
2565    }
2566
2567    fn debug_record_rehydrate(&self, args: Vec<String>) -> VeilidAPIResult<String> {
2568        let registry = self.core_context()?.registry();
2569        let storage_manager = registry.storage_manager();
2570
2571        let key = get_debug_argument_at(
2572            &args,
2573            0,
2574            "debug_record_rehydrate",
2575            "key",
2576            get_dht_key_no_safety,
2577        )?;
2578
2579        let mut rest_defaults = false;
2580
2581        let subkeys = if rest_defaults {
2582            None
2583        } else {
2584            get_debug_argument_at(&args, 1, "debug_record_rehydrate", "subkeys", get_subkeys)
2585                .inspect_err(|_| {
2586                    rest_defaults = true;
2587                })
2588                .ok()
2589        };
2590
2591        let consensus_count = if rest_defaults {
2592            None
2593        } else {
2594            get_debug_argument_at(
2595                &args,
2596                2,
2597                "debug_record_rehydrate",
2598                "consensus_count",
2599                get_number,
2600            )
2601            .inspect_err(|_| {
2602                rest_defaults = true;
2603            })
2604            .ok()
2605        };
2606
2607        // Do a record rehydrate
2608        storage_manager.add_rehydration_request(
2609            key.opaque(),
2610            subkeys.unwrap_or_default(),
2611            consensus_count.unwrap_or_else(|| {
2612                registry.config().internal().network.dht.set_value_count as usize
2613            }),
2614        );
2615
2616        Ok("Request added".to_owned())
2617    }
2618
2619    async fn debug_record(
2620        &self,
2621        command: Option<DebugRecordSubcommand>,
2622    ) -> VeilidAPIResult<String> {
2623        let Some(command) = command else {
2624            apibail_missing_argument!("debug_record", "command");
2625        };
2626
2627        match command {
2628            DebugRecordSubcommand::List { scope } => self.debug_record_list(vec![scope]),
2629            DebugRecordSubcommand::Purge { scope, bytes, keys } => {
2630                if let Some(keys) = keys {
2631                    self.debug_record_purge_keys(scope, keys).await
2632                } else {
2633                    self.debug_record_purge_bytes(scope, bytes).await
2634                }
2635            }
2636            DebugRecordSubcommand::Create {
2637                dht_schema,
2638                crypto_kind,
2639                safety,
2640            } => {
2641                let mut args = Vec::new();
2642                if let Some(dht_schema) = dht_schema {
2643                    args.push(dht_schema);
2644                }
2645                if let Some(crypto_kind) = crypto_kind {
2646                    args.push(crypto_kind);
2647                }
2648                if let Some(safety) = safety {
2649                    args.push(safety);
2650                }
2651                self.debug_record_create(args).await
2652            }
2653            DebugRecordSubcommand::Open { key, writer } => {
2654                let mut args = vec![key];
2655                if let Some(writer) = writer {
2656                    args.push(writer);
2657                }
2658                self.debug_record_open(args).await
2659            }
2660            DebugRecordSubcommand::Close { key } => {
2661                let mut args = Vec::new();
2662                if let Some(key) = key {
2663                    args.push(key);
2664                }
2665                self.debug_record_close(args).await
2666            }
2667            DebugRecordSubcommand::Get { params } => self.debug_record_get(params).await,
2668            DebugRecordSubcommand::Set { params } => self.debug_record_set(params).await,
2669            DebugRecordSubcommand::Delete { key } => self.debug_record_delete(vec![key]).await,
2670            DebugRecordSubcommand::Info { key, subkey } => {
2671                let mut args = vec![key];
2672                if let Some(subkey) = subkey {
2673                    args.push(subkey.to_string());
2674                }
2675                self.debug_record_info(args).await
2676            }
2677            DebugRecordSubcommand::Watch {
2678                key,
2679                subkeys,
2680                expiration,
2681                count,
2682            } => {
2683                let mut args = Vec::new();
2684                if let Some(key) = key {
2685                    args.push(key);
2686                }
2687                if let Some(subkeys) = subkeys {
2688                    args.push(subkeys);
2689                }
2690                if let Some(expiration) = expiration {
2691                    args.push(expiration);
2692                }
2693                if let Some(count) = count {
2694                    args.push(count.to_string());
2695                }
2696                self.debug_record_watch(args).await
2697            }
2698            DebugRecordSubcommand::Cancel { key, subkeys } => {
2699                let mut args = Vec::new();
2700                if let Some(key) = key {
2701                    args.push(key);
2702                }
2703                if let Some(subkeys) = subkeys {
2704                    args.push(subkeys);
2705                }
2706                self.debug_record_cancel(args).await
2707            }
2708            DebugRecordSubcommand::Inspect {
2709                key,
2710                scope,
2711                subkeys,
2712            } => {
2713                let mut args = Vec::new();
2714                if let Some(key) = key {
2715                    args.push(key);
2716                }
2717                if let Some(scope) = scope {
2718                    args.push(scope);
2719                }
2720                if let Some(subkeys) = subkeys {
2721                    args.push(subkeys);
2722                }
2723                self.debug_record_inspect(args).await
2724            }
2725            DebugRecordSubcommand::Rehydrate {
2726                key,
2727                subkeys,
2728                consensus_count,
2729            } => {
2730                let mut args = vec![key];
2731                if let Some(subkeys) = subkeys {
2732                    args.push(subkeys);
2733                }
2734                if let Some(consensus_count) = consensus_count {
2735                    args.push(consensus_count.to_string());
2736                }
2737                self.debug_record_rehydrate(args)
2738            }
2739        }
2740    }
2741
2742    fn debug_table_list(&self) -> VeilidAPIResult<String> {
2743        //
2744        let table_store = self.table_store()?;
2745        let table_names = table_store.list_all();
2746        let out = format!(
2747            "TableStore tables:\n{}",
2748            table_names
2749                .iter()
2750                .map(|(k, v)| format!("{} ({})", k, v))
2751                .collect::<Vec<String>>()
2752                .join("\n")
2753        );
2754        Ok(out)
2755    }
2756
2757    fn _format_columns(columns: &[table_store::ColumnInfo]) -> String {
2758        let mut out = String::new();
2759        for (n, col) in columns.iter().enumerate() {
2760            //
2761            out += &format!("Column {}:\n", n);
2762            out += &format!("  Key Count: {}\n", col.key_count);
2763        }
2764        out
2765    }
2766
2767    async fn debug_table_info(&self, table_name: String) -> VeilidAPIResult<String> {
2768        //
2769        let table_store = self.table_store()?;
2770
2771        let Some(info) = table_store.info(&table_name).await? else {
2772            return Ok(format!("Table '{}' does not exist", table_name));
2773        };
2774
2775        fn debug_print_io_stats(stats: &table_store::IOStatsInfo) -> String {
2776            let mut info_str = format!(
2777                "Started:          {}\n\
2778                Span:             {}\n\
2779                Transactions:     {:6}\n\
2780                Reads:            {:6}\n\
2781                Cache reads:      {:6}\n\
2782                Writes:           {:6}\n\
2783                Bytes read:       {:>6}\n\
2784                Cache bytes read: {:>6}\n\
2785                Bytes written:    {:>6}\n\
2786                Deletes:          {:6}\n\
2787                Prefix deletes:   {:6}\n",
2788                stats.started,
2789                stats.span,
2790                stats.transactions,
2791                stats.reads,
2792                stats.cache_reads,
2793                stats.writes,
2794                bytesize::ByteSize(stats.bytes_read.as_u64())
2795                    .display()
2796                    .iec_short()
2797                    .to_string(),
2798                bytesize::ByteSize(stats.cache_read_bytes.as_u64())
2799                    .display()
2800                    .iec_short()
2801                    .to_string(),
2802                bytesize::ByteSize(stats.bytes_written.as_u64())
2803                    .display()
2804                    .iec_short()
2805                    .to_string(),
2806                stats.deletes,
2807                stats.prefix_deletes,
2808            );
2809
2810            if !stats.write_size_buckets.is_empty() {
2811                info_str += "Write size buckets:\n";
2812                for (size, count) in stats.write_size_buckets.iter() {
2813                    info_str += &format!("  {size:6}: {count:6}\n");
2814                }
2815            }
2816
2817            if !stats.tx_write_size_buckets.is_empty() {
2818                info_str += "Transaction write size buckets:\n";
2819                for (size, (count, avg_duration)) in stats.tx_write_size_buckets.iter() {
2820                    info_str += &format!("  {size:6}: {count:6}  {avg_duration}\n");
2821                }
2822            }
2823
2824            info_str
2825        }
2826
2827        let info_str = format!(
2828            "Table Name: {}\n\
2829            Column Count: {}\n\
2830            IO Stats (since previous query):\n{}\
2831            IO Stats (overall):\n{}\
2832            Columns:\n{}\n",
2833            info.table_name,
2834            info.column_count,
2835            indent_all_by(4, debug_print_io_stats(&info.io_stats_since_previous)),
2836            indent_all_by(4, debug_print_io_stats(&info.io_stats_overall)),
2837            Self::_format_columns(&info.columns),
2838        );
2839
2840        let out = format!("Table info for '{}':\n{}", table_name, info_str);
2841        Ok(out)
2842    }
2843
2844    async fn debug_table(&self, command: Option<DebugTableSubcommand>) -> VeilidAPIResult<String> {
2845        let Some(command) = command else {
2846            apibail_missing_argument!("debug_table", "command");
2847        };
2848
2849        match command {
2850            DebugTableSubcommand::List => self.debug_table_list(),
2851            DebugTableSubcommand::Info { name } => self.debug_table_info(name).await,
2852        }
2853    }
2854
2855    fn debug_punish_list(&self, _args: Vec<String>) -> VeilidAPIResult<String> {
2856        //
2857        let registry = self.core_context()?.registry();
2858        let network_manager = registry.network_manager();
2859        let address_filter = network_manager.address_filter();
2860
2861        let out = format!("Address filter punishments:\n{:#?}", address_filter);
2862        Ok(out)
2863    }
2864
2865    fn debug_punish_clear(&self, _args: Vec<String>) -> VeilidAPIResult<String> {
2866        //
2867        let registry = self.core_context()?.registry();
2868        let network_manager = registry.network_manager();
2869        let address_filter = network_manager.address_filter();
2870
2871        address_filter.clear_punishments();
2872
2873        Ok("Address Filter punishments cleared\n".to_owned())
2874    }
2875
2876    fn debug_punish_add(&self, target: String) -> VeilidAPIResult<String> {
2877        //
2878        let registry = self.core_context()?.registry();
2879        let network_manager = registry.network_manager();
2880        let address_filter = network_manager.address_filter();
2881
2882        if let Some(node_id) = get_node_id(&target) {
2883            address_filter.punish_node_id(node_id, PunishmentReason::Manual);
2884            Ok("Address Filter node id punishment added\n".to_owned())
2885        } else if let Some(ip_addr) = get_ip_addr(&target) {
2886            address_filter.punish_ip_addr(ip_addr, PunishmentReason::Manual);
2887            Ok("Address Filter address punishment added\n".to_owned())
2888        } else {
2889            apibail_invalid_argument!("debug_punish_add", "target", target);
2890        }
2891    }
2892
2893    fn debug_punish_remove(&self, target: String) -> VeilidAPIResult<String> {
2894        //
2895        let registry = self.core_context()?.registry();
2896        let network_manager = registry.network_manager();
2897        let address_filter = network_manager.address_filter();
2898
2899        if let Some(node_id) = get_node_id(&target) {
2900            address_filter.forgive_node_id(node_id);
2901            Ok("Address Filter node id punishment forgiven\n".to_owned())
2902        } else if let Some(ip_addr) = get_ip_addr(&target) {
2903            address_filter.forgive_ip_addr(ip_addr);
2904            Ok("Address Filter address punishment forgiven\n".to_owned())
2905        } else {
2906            apibail_invalid_argument!("debug_punish_remove", "target", target);
2907        }
2908    }
2909
2910    fn debug_punish(&self, command: Option<DebugPunishSubcommand>) -> VeilidAPIResult<String> {
2911        let Some(command) = command else {
2912            apibail_missing_argument!("debug_punish", "command");
2913        };
2914
2915        match command {
2916            DebugPunishSubcommand::List => self.debug_punish_list(vec![]),
2917            DebugPunishSubcommand::Clear => self.debug_punish_clear(vec![]),
2918            DebugPunishSubcommand::Add { target } => self.debug_punish_add(target),
2919            DebugPunishSubcommand::Remove { target } => self.debug_punish_remove(target),
2920        }
2921    }
2922
2923    /// Get the help text for 'internal debug' commands.
2924    pub fn debug_help(&self) -> VeilidAPIResult<String> {
2925        let out = render_parser_help::<DebugCommandParser>();
2926        let out = out
2927            .lines()
2928            .map(|line| line.trim().to_string())
2929            .collect::<Vec<String>>()
2930            .join("\n");
2931        Ok(out)
2932    }
2933
2934    /// Get node uptime info.
2935    pub async fn debug_uptime(&self) -> VeilidAPIResult<String> {
2936        let mut result = String::new();
2937
2938        writeln!(result, "Uptime...").ok();
2939
2940        let state = self.get_state().await?;
2941
2942        let uptime = state.attachment.uptime;
2943        writeln!(result, "  since launch: {uptime:#}").ok();
2944
2945        if let Some(attached_uptime) = state.attachment.attached_uptime {
2946            writeln!(result, "  since attachment: {attached_uptime:#}").ok();
2947        }
2948
2949        Ok(result)
2950    }
2951
2952    /// Cause veilid-core to panic via various means
2953    #[cfg(debug_assertions)]
2954    #[expect(clippy::unused_async)]
2955    pub async fn debug_die(
2956        &self,
2957        mode: Option<String>,
2958        message: Vec<String>,
2959    ) -> VeilidAPIResult<String> {
2960        let arg = mode.as_deref().unwrap_or_default();
2961        let rest = join_args(message);
2962        match arg {
2963            "panic" => {
2964                if rest.is_empty() {
2965                    panic!();
2966                } else {
2967                    panic!("{}", rest);
2968                }
2969            }
2970            "unwrap" => {
2971                #[expect(clippy::unnecessary_literal_unwrap)]
2972                Option::<()>::None.unwrap();
2973                unreachable!("unwrap must panic");
2974            }
2975            "unwrap_or_log" => {
2976                Option::<()>::None.unwrap_or_log();
2977                unreachable!("unwrap_or_log must panic");
2978            }
2979            "expect" => {
2980                #[expect(clippy::unnecessary_literal_unwrap)]
2981                Option::<()>::None.expect(&rest);
2982                unreachable!("expect must panic");
2983            }
2984            "expect_or_log" => {
2985                Option::<()>::None.expect_or_log(&rest);
2986                unreachable!("expect_or_log must panic");
2987            }
2988            "div0" => {
2989                let x = 0u32;
2990                let y = x / 0u32;
2991                unreachable!("divide by zero must panic: {}", y);
2992            }
2993            "overflow" => {
2994                let x = u32::MAX;
2995                let y = x + 1;
2996                unreachable!("integer overflow must panic: {}", y);
2997            }
2998            "oob" => {
2999                let x = &[3];
3000                #[expect(clippy::out_of_bounds_indexing)]
3001                let y = x[1];
3002                unreachable!("array out of bounds must panic: {}", y);
3003            }
3004            "nullptr" => {
3005                let x: *const i32 = std::ptr::null();
3006                let y = unsafe { *x };
3007                unreachable!("array out of bounds must panic: {}", y);
3008            }
3009            "unreachable" => {
3010                unreachable!("direct call of unreachable macro");
3011            }
3012            _ => {
3013                Ok("One of 'panic [message]', 'unwrap', 'unwrap_or_log', 'expect [message]', 'expect_or_log [message]', 'div0', 'overflow', 'oob', 'nullptr', or 'unreachable' is required".to_string())
3014            }
3015        }
3016    }
3017
3018    /// Execute an internal debug command.
3019    /// `appmessage`, `appcall`, and `appreply` preserve raw trailing payload
3020    /// text and parse payloads in command-specific logic. Buckets/config/purge/
3021    /// route/record/table/punish/network command routing uses clap while
3022    /// semantic parsing remains delegated to existing helpers and downstream
3023    /// components.
3024    pub async fn debug(&self, args: String) -> VeilidAPIResult<String> {
3025        let res = {
3026            let argv_parts = shell_words::split(&args)
3027                .map_err(|e| VeilidAPIError::parse_error(e.to_string(), args))?;
3028
3029            if argv_parts.is_empty() || argv_parts.len() == 1 && argv_parts[0] == "help" {
3030                return self.debug_help();
3031            }
3032
3033            let argv: Vec<String> = std::iter::once("debug".to_owned())
3034                .chain(argv_parts.iter().cloned())
3035                .collect();
3036            let nested_family =
3037                get_nested_debug_family_name(argv_parts.first().map(String::as_str));
3038            let known_command =
3039                get_known_debug_command_name(argv_parts.first().map(String::as_str));
3040
3041            let parsed = match DebugCommandParser::try_parse_from(argv) {
3042                Ok(parsed) => parsed,
3043                Err(err) if err.kind() == ErrorKind::DisplayHelp => return Ok(err.to_string()),
3044                Err(err) if err.kind() == ErrorKind::InvalidSubcommand => {
3045                    if let Some(family) = nested_family {
3046                        return Err(VeilidAPIError::generic(format!(
3047                            "Unknown {} subcommand",
3048                            family
3049                        )));
3050                    }
3051                    return Err(VeilidAPIError::generic("Unknown debug command"));
3052                }
3053                Err(_) => {
3054                    if let Some(family) = known_command {
3055                        let arg = argv_parts.get(1).cloned().unwrap_or_default();
3056                        apibail_invalid_argument!(format!("debug_{}", family), "arg_1", arg);
3057                    } else {
3058                        return Err(VeilidAPIError::generic("Unknown debug command"));
3059                    }
3060                }
3061            };
3062
3063            let command = parsed.command;
3064
3065            match command {
3066                DebugCommand::Nodeid => self.debug_nodeid(),
3067                DebugCommand::Buckets { min_state } => self.debug_buckets(min_state),
3068                DebugCommand::Dialinfo => self.debug_dialinfo(),
3069                DebugCommand::Peerinfo { args } => self.debug_peerinfo(args),
3070                DebugCommand::Contact { node_ref } => self.debug_contact(node_ref),
3071                DebugCommand::Keypair { cryptokind } => self.debug_keypair(cryptokind),
3072                DebugCommand::Entries { args } => self.debug_entries(args),
3073                DebugCommand::Entry { node } => self.debug_entry(node),
3074                DebugCommand::Punish { command } => self.debug_punish(command),
3075                DebugCommand::Txtrecord { keypairs } => self.debug_txtrecord(keypairs).await,
3076                DebugCommand::Relay { args } => self.debug_relay(args).await,
3077                DebugCommand::Ping { destination } => self.debug_ping(destination).await,
3078                DebugCommand::Appmessage { destination, data } => {
3079                    self.debug_app_message(destination, data).await
3080                }
3081                DebugCommand::Appcall { destination, data } => {
3082                    self.debug_app_call(destination, data).await
3083                }
3084                DebugCommand::Appreply { id, data } => self.debug_app_reply(id, data).await,
3085                DebugCommand::Resolve { destination } => self.debug_resolve(destination).await,
3086                DebugCommand::Nodeinfo => self.debug_nodeinfo().await,
3087                DebugCommand::Purge { command } => self.debug_purge(command).await,
3088                DebugCommand::Attach => self.debug_attach().await,
3089                DebugCommand::Detach => self.debug_detach().await,
3090                DebugCommand::Config { arg_0, arg_1 } => self.debug_config(arg_0, arg_1).await,
3091                DebugCommand::Network { command } => self.debug_network(command).await,
3092                DebugCommand::Route { command } => self.debug_route(command).await,
3093                DebugCommand::Record { command } => self.debug_record(command).await,
3094                DebugCommand::Table { command } => self.debug_table(command).await,
3095                DebugCommand::Uptime => self.debug_uptime().await,
3096                #[cfg(debug_assertions)]
3097                DebugCommand::Die { mode, message } => self.debug_die(mode, message).await,
3098            }
3099        };
3100        res
3101    }
3102
3103    fn get_destination(
3104        self,
3105        registry: VeilidComponentRegistry,
3106    ) -> impl FnOnce(&str) -> PinBoxFutureStatic<Option<Destination>> {
3107        move |text| {
3108            let text = text.to_owned();
3109            Box::pin(async move {
3110                // Safety selection
3111                let (text, ss) = if let Some((first, second)) = text.split_once('+') {
3112                    let ss = get_safety_selection(registry.clone())(second)?;
3113                    (first, Some(ss))
3114                } else {
3115                    (text.as_str(), None)
3116                };
3117                if text.is_empty() {
3118                    return None;
3119                }
3120                if &text[0..1] == "#" {
3121                    let routing_table = registry.routing_table();
3122                    let rss = routing_table.route_spec_store();
3123
3124                    // Private route
3125                    let text = &text[1..];
3126
3127                    let private_route = if let Some(prid) =
3128                        get_route_id(registry.clone(), false, true)(text)
3129                    {
3130                        rss.best_remote_private_route(&RemoteRouteSetId::from_route_id(prid))?
3131                    } else {
3132                        let n = get_number(text)?;
3133
3134                        self.with_debug_cache(|dc| {
3135                            let prid: &RouteId = dc.imported_routes.get(n)?;
3136                            let prid = RemoteRouteSetId::from_route_id(prid.clone());
3137                            let Some(private_route) = rss.best_remote_private_route(&prid) else {
3138                                // Remove imported route
3139                                let _ = dc.imported_routes.remove(n);
3140                                veilid_log!(registry info "removed dead imported route {}", n);
3141                                return None;
3142                            };
3143                            Some(private_route)
3144                        })?
3145                    };
3146
3147                    Some(Destination::private_route(
3148                        private_route,
3149                        ss.unwrap_or(SafetySelection::Unsafe(Sequencing::default())),
3150                    ))
3151                } else if let Some((first, second)) = text.split_once('@') {
3152                    if ss.is_some() {
3153                        return None;
3154                    }
3155                    // Relay
3156                    let relay_di = DialInfo::from_str(second).ok()?;
3157                    let target_nr = get_node_ref(registry.clone())(first)?;
3158
3159                    Some(Destination::dial_info(relay_di, target_nr))
3160                } else {
3161                    // Direct
3162                    let target_nr = resolve_filtered_node_ref(
3163                        registry.clone(),
3164                        ss.clone()
3165                            .unwrap_or(SafetySelection::Unsafe(Sequencing::PreferUnordered)),
3166                    )(text)
3167                    .await?;
3168
3169                    Some(Destination::direct(target_nr, ss))
3170                }
3171            })
3172        }
3173    }
3174
3175    fn get_opened_dht_record_context(
3176        self,
3177        args: &[String],
3178        context: &str,
3179        key: &str,
3180        arg: usize,
3181    ) -> VeilidAPIResult<(RecordKey, RoutingContext)> {
3182        let key = match get_debug_argument_at(args, arg, context, key, get_dht_key_no_safety)
3183            .ok()
3184            .or_else(|| {
3185                // If unspecified, use the most recent key opened or created
3186                self.with_debug_cache(|dc| dc.opened_record_contexts.back().map(|kv| kv.0).cloned())
3187            }) {
3188            Some(k) => k,
3189            None => {
3190                apibail_missing_argument!("no keys are opened", "key");
3191            }
3192        };
3193
3194        // Get routing context for record
3195
3196        let Some(rc) = self.with_debug_cache(|dc| dc.opened_record_contexts.get(&key).cloned())
3197        else {
3198            apibail_missing_argument!("key is not opened", "key");
3199        };
3200
3201        Ok((key, rc))
3202    }
3203}
3204
3205#[cfg(test)]
3206mod clap_parse_tests {
3207    use super::*;
3208
3209    #[test]
3210    fn parses_top_level_commands() {
3211        let parsed = DebugCommandParser::try_parse_from(["debug", "nodeinfo"]).ok();
3212        assert!(matches!(
3213            parsed,
3214            Some(DebugCommandParser {
3215                command: DebugCommand::Nodeinfo
3216            })
3217        ));
3218
3219        let parsed = DebugCommandParser::try_parse_from(["debug", "nodeid"]).ok();
3220        assert!(matches!(
3221            parsed,
3222            Some(DebugCommandParser {
3223                command: DebugCommand::Nodeid
3224            })
3225        ));
3226
3227        assert!(DebugCommandParser::try_parse_from(["debug", "NoDeId"]).is_err());
3228    }
3229
3230    #[test]
3231    fn rejects_unknown_top_level_command() {
3232        assert!(DebugCommandParser::try_parse_from(["debug", "definitely-unknown"]).is_err());
3233    }
3234
3235    #[test]
3236    fn single_pass_parser_preserves_quoted_payload_tokens() {
3237        let parsed =
3238            DebugCommandParser::try_parse_from(["debug", "appmessage", "destination", "foo bar"])
3239                .ok();
3240        assert!(matches!(
3241            parsed,
3242            Some(DebugCommandParser {
3243                command: DebugCommand::Appmessage { .. }
3244            })
3245        ));
3246    }
3247
3248    #[test]
3249    fn parses_route_subcommands() {
3250        let parsed = DebugCommandParser::try_parse_from(["debug", "route", "allocate", "rel"]).ok();
3251        assert!(matches!(
3252            parsed,
3253            Some(DebugCommandParser {
3254                command: DebugCommand::Route {
3255                    command: Some(DebugRouteSubcommand::Allocate { .. })
3256                }
3257            })
3258        ));
3259
3260        let parsed =
3261            DebugCommandParser::try_parse_from(["debug", "route", "test", "route-id"]).ok();
3262        assert!(matches!(
3263            parsed,
3264            Some(DebugCommandParser {
3265                command: DebugCommand::Route {
3266                    command: Some(DebugRouteSubcommand::Test { .. })
3267                }
3268            })
3269        ));
3270    }
3271
3272    #[test]
3273    fn rejects_unknown_route_subcommand() {
3274        assert!(DebugCommandParser::try_parse_from(["debug", "route", "unknown"]).is_err());
3275    }
3276
3277    #[test]
3278    fn parses_record_subcommands() {
3279        let parsed = DebugCommandParser::try_parse_from(["debug", "record", "create", "1"]).ok();
3280        assert!(matches!(
3281            parsed,
3282            Some(DebugCommandParser {
3283                command: DebugCommand::Record {
3284                    command: Some(DebugRecordSubcommand::Create { .. })
3285                }
3286            })
3287        ));
3288
3289        let parsed =
3290            DebugCommandParser::try_parse_from(["debug", "record", "rehydrate", "some-key"]).ok();
3291        assert!(matches!(
3292            parsed,
3293            Some(DebugCommandParser {
3294                command: DebugCommand::Record {
3295                    command: Some(DebugRecordSubcommand::Rehydrate { .. })
3296                }
3297            })
3298        ));
3299    }
3300
3301    #[test]
3302    fn parses_table_punish_and_network_subcommands() {
3303        let parsed = DebugCommandParser::try_parse_from(["debug", "table", "info", "table"]).ok();
3304        assert!(matches!(
3305            parsed,
3306            Some(DebugCommandParser {
3307                command: DebugCommand::Table {
3308                    command: Some(DebugTableSubcommand::Info { .. })
3309                }
3310            })
3311        ));
3312
3313        let parsed = DebugCommandParser::try_parse_from(["debug", "punish", "remove", "node"]).ok();
3314        assert!(matches!(
3315            parsed,
3316            Some(DebugCommandParser {
3317                command: DebugCommand::Punish {
3318                    command: Some(DebugPunishSubcommand::Remove { .. })
3319                }
3320            })
3321        ));
3322
3323        let parsed = DebugCommandParser::try_parse_from(["debug", "network", "stats"]).ok();
3324        assert!(matches!(
3325            parsed,
3326            Some(DebugCommandParser {
3327                command: DebugCommand::Network {
3328                    command: Some(DebugNetworkSubcommand::Stats)
3329                }
3330            })
3331        ));
3332
3333        let parsed = DebugCommandParser::try_parse_from(["debug", "purge", "routes"]).ok();
3334        assert!(matches!(
3335            parsed,
3336            Some(DebugCommandParser {
3337                command: DebugCommand::Purge {
3338                    command: Some(DebugPurgeSubcommand::Routes)
3339                }
3340            })
3341        ));
3342    }
3343
3344    #[test]
3345    fn parses_missing_nested_subcommands() {
3346        let parsed = DebugCommandParser::try_parse_from(["debug", "route"]).ok();
3347        assert!(matches!(
3348            parsed,
3349            Some(DebugCommandParser {
3350                command: DebugCommand::Route { command: None }
3351            })
3352        ));
3353
3354        let parsed = DebugCommandParser::try_parse_from(["debug", "record"]).ok();
3355        assert!(matches!(
3356            parsed,
3357            Some(DebugCommandParser {
3358                command: DebugCommand::Record { command: None }
3359            })
3360        ));
3361
3362        let parsed = DebugCommandParser::try_parse_from(["debug", "purge"]).ok();
3363        assert!(matches!(
3364            parsed,
3365            Some(DebugCommandParser {
3366                command: DebugCommand::Purge { command: None }
3367            })
3368        ));
3369    }
3370
3371    #[test]
3372    fn parses_buckets_config_and_die_forms() {
3373        let parsed = DebugCommandParser::try_parse_from(["debug", "buckets", "dead"]).ok();
3374        assert!(matches!(
3375            parsed,
3376            Some(DebugCommandParser {
3377                command: DebugCommand::Buckets { min_state: Some(_) }
3378            })
3379        ));
3380
3381        let parsed = DebugCommandParser::try_parse_from(["debug", "config", "insecure"]).ok();
3382        assert!(matches!(
3383            parsed,
3384            Some(DebugCommandParser {
3385                command: DebugCommand::Config {
3386                    arg_0: Some(_),
3387                    arg_1: None
3388                }
3389            })
3390        ));
3391
3392        #[cfg(debug_assertions)]
3393        {
3394            let parsed = DebugCommandParser::try_parse_from(["debug", "die", "panic", "msg"]).ok();
3395            assert!(matches!(
3396                parsed,
3397                Some(DebugCommandParser {
3398                    command: DebugCommand::Die {
3399                        mode: Some(_),
3400                        message
3401                    }
3402                }) if message.len() == 1
3403            ));
3404        }
3405    }
3406
3407    #[test]
3408    fn parses_record_data_forms() {
3409        assert_eq!(get_data("#666f6f"), Some(b"foo".to_vec()));
3410        assert_eq!(get_data("\"foo\\nbar\""), Some(b"foo\nbar".to_vec()));
3411        assert_eq!(get_data("plain"), Some(b"plain".to_vec()));
3412        assert!(parse_data("#zz").is_err());
3413    }
3414
3415    #[test]
3416    fn parses_context_free_converters() {
3417        assert!(matches!(
3418            parse_bucket_entry_state("dead"),
3419            Ok(BucketEntryState::Dead)
3420        ));
3421        assert!(matches!(
3422            parse_dht_report_scope("syncset"),
3423            Ok(DHTReportScope::SyncSet)
3424        ));
3425        assert!(PublishedState::from_str("published")
3426            .map(|x| x.as_bool())
3427            .unwrap_or(false));
3428        assert!(parse_subkeys("1..=3,5..=8").is_ok());
3429    }
3430
3431    #[test]
3432    fn generated_help_includes_command_groups() {
3433        let help = render_parser_help::<DebugCommandParser>();
3434        assert!(help.contains("nodeid"));
3435        assert!(help.contains("record"));
3436        assert!(help.contains("help"));
3437    }
3438
3439    #[test]
3440    fn top_level_help_subcommand_is_available() {
3441        let err = DebugCommandParser::try_parse_from(["debug", "help", "route"]).unwrap_err();
3442        assert_eq!(err.kind(), ErrorKind::DisplayHelp);
3443    }
3444}