44
55//! ## `humility rpc`
66//!
7- //! `humility rpc` allows for execution of Idol commands over a network, rather
8- //! than through a debugger.
9- //!
10- //! It requires the Hubris `udprpc` task to be listening on port 8. This task
11- //! decodes bytes from a UDP packet, and shoves them directly into `sys_send` to
12- //! a target task.
13- //!
14- //! An archive is required so that `humility` knows what functions are available
15- //! and how to call them. The archive ID is checked against the image ID on the
16- //! target; `udprpc` will refuse to execute commands when the ID does not match.
17- //!
18- //! Function calls are handled identically to the `humility hiffy` subcommand,
19- //! except that an `--ip` address is required:
20- //!
21- //! ```console
22- //! $ humility rpc --ip fe80::0c1d:9aff:fe64:b8c2%en0 -c UserLeds.led_on -aindex=0
23- //! UserLeds.led_on() = ()
24- //! ```
25- //!
26- //! Alternatively, you can set the `HUMILITY_RPC_IP` environmental variable.
7+ //! `humility rpc` lets you discover SPs on a network, instead of using a
8+ //! physically attached debugger. Once SPs are discovered, they may be used as
9+ //! a target by setting `HUMILITY_IP` or providing the `--ip` argument.
2710//!
2811//! You may need to configure an IPv6 network for `humility rpc` to work. On
2912//! illumos, it looks like this:
5235//! not include identity information. If they are marked as `(vpdfail)`, they
5336//! are running a new-enough `udpbroadcast`, but the SP was unable to read its
5437//! identity from its VPD.
55- //!
56- //! To call all targets that match an archive, `--listen` can be combined with
57- //! `--call`
5838
5939use std:: collections:: BTreeSet ;
60- use std:: net:: { IpAddr , Ipv6Addr , ToSocketAddrs , UdpSocket } ;
40+ use std:: net:: { IpAddr , Ipv6Addr , UdpSocket } ;
6141use std:: time:: { Duration , Instant } ;
6242
63- use anyhow:: { Context , Result , anyhow , bail} ;
43+ use anyhow:: { Result , bail} ;
6444use clap:: { ArgGroup , IntoApp , Parser } ;
6545use colored:: Colorize ;
6646use hubpack:: SerializedSize ;
67- use humility:: net:: { ScopedV6Addr , decode_iface} ;
68- use humility:: { hubris:: * , reflect} ;
47+ use humility:: net:: decode_iface;
6948use humility_cli:: { ExecutionContext , Subcommand } ;
7049use humility_cmd:: { Archive , Command , CommandKind } ;
71- use humility_doppel:: RpcHeader ;
72- use humility_idol as idol;
7350use serde:: { Deserialize , Serialize } ;
74- use zerocopy:: { AsBytes , U16 , U64 } ;
7551
7652#[ derive( Parser , Debug ) ]
7753#[ clap(
@@ -90,10 +66,6 @@ struct RpcArgs {
9066 #[ clap( long, short) ]
9167 list : bool ,
9268
93- /// call a particular function
94- #[ clap( long, short, conflicts_with = "list" , requires = "target" ) ]
95- call : Option < String > ,
96-
9769 /// listen for compatible SPs on the network
9870 #[ clap(
9971 long,
@@ -103,26 +75,9 @@ struct RpcArgs {
10375 ) ]
10476 listen : bool ,
10577
106- /// arguments
107- #[ clap( long, short, requires = "call" ) ]
108- task : Option < String > ,
109-
11078 /// interface on which to listen, e.g. 'en0'
11179 #[ clap( short, requires = "listen" , value_parser = decode_iface) ]
11280 interface : Option < u32 > ,
113-
114- /// arguments
115- #[ clap( long, short, requires = "call" , use_delimiter = true ) ]
116- arguments : Vec < String > ,
117-
118- /// IPv6 addresses, e.g. `fe80::0c1d:9aff:fe64:b8c2%en0`
119- #[ clap(
120- long,
121- env = "HUMILITY_RPC_IP" ,
122- group = "target" ,
123- use_value_delimiter = true
124- ) ]
125- ip : Option < Vec < ScopedV6Addr > > ,
12681}
12782
12883#[ derive( Debug , Clone , Copy , Deserialize , Serialize , SerializedSize ) ]
@@ -360,209 +315,18 @@ fn rpc_dump(seen: BTreeSet<Target>, image_id: &[u8]) {
360315 }
361316}
362317
363- pub struct RpcClient < ' a > {
364- hubris : & ' a HubrisArchive ,
365- socket : UdpSocket ,
366- rpc_reply_type : & ' a HubrisEnum ,
367- buf : Vec < u8 > , // sized to match socket buffer size
368- }
369-
370- impl < ' a > RpcClient < ' a > {
371- pub fn new (
372- hubris : & ' a HubrisArchive ,
373- ip : ScopedV6Addr ,
374- timeout : Duration ,
375- ) -> Result < Self > {
376- let udprpc = hubris. manifest . get_socket_by_task ( "udprpc" ) ?;
377- let target = format ! ( "[{ip}]:{}" , udprpc. port) ;
378-
379- let dest = target. to_socket_addrs ( ) ?. collect :: < Vec < _ > > ( ) ;
380- let socket = UdpSocket :: bind ( "[::]:0" ) ?;
381- socket. set_read_timeout ( Some ( timeout) ) ?;
382- socket. connect ( & dest[ ..] ) ?;
383-
384- let rpc_task = hubris. lookup_task ( "udprpc" ) . ok_or_else ( || {
385- anyhow ! (
386- "Could not find `udprpc` task in this image. \
387- Only -dev and -lab images include `udprpc`; \
388- are you running a production image?"
389- )
390- } ) ?;
391- let rpc_reply_type = hubris
392- . lookup_module ( rpc_task) ?
393- . lookup_enum_byname ( hubris, "RpcReply" ) ?
394- . ok_or_else ( || anyhow ! ( "can't find RpcReply" ) ) ?;
395-
396- let buf = vec ! [ 0u8 ; udprpc. tx. bytes] ;
397- Ok ( Self { hubris, socket, rpc_reply_type, buf } )
398- }
399-
400- pub fn call (
401- & mut self ,
402- op : & idol:: IdolOperation ,
403- args : & [ ( & str , idol:: IdolArgument ) ] ,
404- ) -> Result < Result < reflect:: Value , String > > {
405- let payload = op. payload ( args) ?;
406-
407- let our_image_id = self . hubris . image_id ( ) . unwrap ( ) ;
408-
409- let nreply = op. reply_size ( ) ?;
410-
411- let header = RpcHeader {
412- image_id : U64 :: from_bytes ( our_image_id. try_into ( ) . unwrap ( ) ) ,
413- task : U16 :: new ( op. task . task ( ) . try_into ( ) . unwrap ( ) ) ,
414- op : U16 :: new ( op. code ) ,
415- nreply : U16 :: new ( nreply as u16 ) ,
416- nbytes : U16 :: new ( payload. len ( ) . try_into ( ) . unwrap ( ) ) ,
417- } ;
418- let mut packet = header. as_bytes ( ) . to_vec ( ) ;
419- packet. extend ( payload. iter ( ) ) ;
420-
421- self . socket . send ( & packet) . context ( "unable to send packet" ) ?;
422- let n = self
423- . socket
424- . recv ( & mut self . buf )
425- . context ( "unable to receive packet" ) ?;
426- let buf = & self . buf [ ..n] ;
427-
428- if buf[ 0 ] != 0 {
429- // TODO: assumes the discriminator is a u8. It's not clear from
430- // context whether this assumption carries through into the udprpc
431- // task.
432- match self . rpc_reply_type . lookup_variant_by_tag ( Tag :: from ( buf[ 0 ] ) ) {
433- Some ( e) => {
434- let msg = format ! ( "Got error from `udprpc`: {}" , e. name) ;
435- if e. name == "BadImageId" {
436- bail ! (
437- "{msg}: {:02x?} (Humility) {:02x?} (Hubris)" ,
438- our_image_id,
439- & buf[ 1 ..9 ]
440- ) ;
441- } else {
442- bail ! ( "{msg}" ) ;
443- }
444- }
445- None => bail ! ( "Got unknown error from `udprpc`: {}" , buf[ 0 ] ) ,
446- }
447- } else {
448- // Check the return code from the Idol call
449- let rc = u32:: from_be_bytes ( buf[ 1 ..5 ] . try_into ( ) . unwrap ( ) ) ;
450- let val = if rc == 0 { Ok ( buf[ 5 ..] . to_vec ( ) ) } else { Err ( rc) } ;
451- let result = humility_hiffy:: hiffy_decode ( self . hubris , op, val) ?;
452- Ok ( result)
453- }
454- }
455- }
456-
457- fn rpc_call (
458- hubris : & HubrisArchive ,
459- op : & idol:: IdolOperation ,
460- args : & [ ( & str , idol:: IdolArgument ) ] ,
461- ips : Vec < ScopedV6Addr > ,
462- timeout : u32 ,
463- ) -> Result < ( ) > {
464- let timeout = Duration :: from_millis ( u64:: from ( timeout) ) ;
465-
466- for & ip in & ips {
467- let mut client = RpcClient :: new ( hubris, ip, timeout)
468- . context ( "unable to create RpcClient" ) ?;
469- let result = client. call ( op, args) . context ( "unable to make call" ) ?;
470- print ! ( "{:25} " , ip) ;
471- humility_hiffy:: hiffy_print_result ( hubris, op, result)
472- . context ( "unable to print result" ) ?;
473- }
474-
475- Ok ( ( ) )
476- }
477-
478318fn rpc_run ( context : & mut ExecutionContext ) -> Result < ( ) > {
479319 let Subcommand :: Other ( subargs) = context. cli . cmd . as_ref ( ) . unwrap ( ) ;
480320 let subargs = RpcArgs :: try_parse_from ( subargs) ?;
481321 let hubris = context. archive . as_ref ( ) . unwrap ( ) ;
482322
483- if subargs. list {
484- cmd_hiffy:: hiffy_list ( hubris, vec ! [ ] ) ?;
485- return Ok ( ( ) ) ;
486- }
487-
488- if subargs. listen && subargs. call . is_none ( ) {
323+ // We previously had more subcommands here, but are down to just `--listen`
324+ if subargs. listen {
489325 rpc_dump ( rpc_listen ( & subargs) ?, hubris. image_id ( ) . unwrap ( ) ) ;
490- return Ok ( ( ) ) ;
491- }
492-
493- // For some reason, macOS requires the interface to be non-zero:
494- // https://users.rust-lang.org/t/ipv6-upnp-multicast-for-rust-dlna-server-macos/24425
495- // https://bluejekyll.github.io/blog/posts/multicasting-in-rust/
496- let interface = match subargs. interface {
497- None => {
498- if cfg ! ( target_os = "macos" ) {
499- bail ! ( "Must specify interface with `-i` on macOS" ) ;
500- } else {
501- 0
502- }
503- }
504- Some ( iface) => iface,
505- } ;
506-
507- let ips = if subargs. listen {
508- let image_id = hubris. image_id ( ) . unwrap ( ) ;
509-
510- rpc_listen ( & subargs) ?
511- . iter ( )
512- . filter ( |t| t. image_id == image_id)
513- . map ( |target| match target. ip {
514- IpAddr :: V4 ( ip) => bail ! (
515- "target {target:?} had an unanticipated \
516- IPv4 address ({ip})"
517- ) ,
518- IpAddr :: V6 ( ip) => Ok ( ScopedV6Addr { ip, scopeid : interface } ) ,
519- } )
520- . collect :: < Result < Vec < _ > > > ( ) ?
326+ Ok ( ( ) )
521327 } else {
522- subargs. ip . ok_or_else ( || {
523- anyhow ! (
524- "the `--ip <IPS>` argument is required by `humility rpc` \
525- unless `--listen` is also set"
526- )
527- } ) ?
528- } ;
529-
530- if let Some ( call) = & subargs. call {
531- if hubris. lookup_task ( "udprpc" ) . is_none ( ) {
532- bail ! ( "no `udprpc` task in the target image" ) ;
533- }
534-
535- let func: Vec < & str > = call. split ( '.' ) . collect ( ) ;
536-
537- if func. len ( ) != 2 {
538- bail ! ( "calls must be interface.operation (-l to list)" ) ;
539- }
540-
541- let mut args = vec ! [ ] ;
542-
543- for arg in & subargs. arguments {
544- let arg: Vec < & str > = arg. split ( '=' ) . collect ( ) ;
545-
546- if arg. len ( ) != 2 {
547- bail ! ( "arguments must be argument=value (-l to list)" ) ;
548- }
549-
550- args. push ( ( arg[ 0 ] , idol:: IdolArgument :: String ( arg[ 1 ] ) ) ) ;
551- }
552-
553- let task = match & subargs. task {
554- Some ( task) => Some ( hubris. try_lookup_task ( task) ?) ,
555- None => None ,
556- } ;
557-
558- let op = idol:: IdolOperation :: new ( hubris, func[ 0 ] , func[ 1 ] , task) ?;
559- rpc_call ( hubris, & op, & args, ips, subargs. timeout )
560- . context ( "failed to make RPC call" ) ?;
561-
562- return Ok ( ( ) ) ;
328+ bail ! ( "expected --listen" )
563329 }
564-
565- bail ! ( "expected --listen, --list, or --call" )
566330}
567331
568332pub fn init ( ) -> Command {
0 commit comments